Merge tm-dev-plus-aosp-without-vendor@8763363

Bug: 236760014
Merged-In: I17b0b3e8ef2eb0ce62c26a61ef26d1067a18edb6
Change-Id: Ie2e257ae14e0eb5d287f15d1db7c7f90485e6bd9
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..d69dc2c
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,20 @@
+//
+// 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.
+//
+
+package {
+    default_visibility: [":__subpackages__"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..ef2ebb1
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,12 @@
+[Builtin Hooks]
+xmllint = true
+clang_format = true
+commit_msg_changeid_field = true
+rustfmt = true
+
+[Builtin Hooks Options]
+clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
+rustfmt = --config-path=rustfmt.toml
+
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
diff --git a/TEST_MAPPING b/TEST_MAPPING
new file mode 100644
index 0000000..d251bea
--- /dev/null
+++ b/TEST_MAPPING
@@ -0,0 +1,19 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsUwbTestCases"
+    },
+    {
+      "name": "FrameworkUwbTests"
+    },
+    {
+      "name": "ServiceUwbTests"
+    },
+    {
+      "name": "UwbSupportLibTests"
+    },
+    {
+      "name": "libuwb_uci_jni_rust_tests"
+    }
+  ]
+}
diff --git a/apex/Android.bp b/apex/Android.bp
index 4bda0bb..b8ae3de 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -18,10 +18,34 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+apex_defaults {
+    name: "com.android.uwb-defaults",
+    bootclasspath_fragments: ["com.android.uwb-bootclasspath-fragment"],
+    systemserverclasspath_fragments: ["com.android.uwb-systemserverclasspath-fragment"],
+    multilib: {
+        both: {
+            jni_libs: [
+               "libuwb_uci_jni_rust",
+            ],
+        },
+    },
+    apps: [
+        "ServiceUwbResources",
+    ],
+    key: "com.android.uwb.key",
+    certificate: ":com.android.uwb.certificate",
+    defaults: ["t-launched-apex-module"],
+
+    // Indicates that pre-installed version of this apex can be compressed.
+    // Whether it actually will be compressed is controlled on per-device basis.
+    compressible: true,
+}
+
+// Mainline uwb apex module.
 apex {
     name: "com.android.uwb",
-    key: "com.android.uwb.key",
-    defaults: ["t-launched-apex-module"],
+    defaults: ["com.android.uwb-defaults"],
+    manifest: "apex_manifest.json",
 }
 
 apex_key {
@@ -29,3 +53,64 @@
     public_key: "com.android.uwb.avbpubkey",
     private_key: "com.android.uwb.pem",
 }
+
+android_app_certificate {
+    name: "com.android.uwb.certificate",
+    certificate: "com.android.uwb",
+}
+
+sdk {
+    name: "uwb-module-sdk",
+    bootclasspath_fragments: ["com.android.uwb-bootclasspath-fragment"],
+    systemserverclasspath_fragments: ["com.android.uwb-systemserverclasspath-fragment"],
+}
+
+// Encapsulate the contributions made by the com.android.uwb to the bootclasspath.
+bootclasspath_fragment {
+    name: "com.android.uwb-bootclasspath-fragment",
+    contents: ["framework-uwb"],
+    apex_available: ["com.android.uwb"],
+
+    // The bootclasspath_fragments that provide APIs on which this depends.
+    fragments: [
+        {
+            apex: "com.android.art",
+            module: "art-bootclasspath-fragment",
+        },
+    ],
+
+    // Additional stubs libraries that this fragment's contents use which are
+    // not provided by another bootclasspath_fragment.
+    additional_stubs: [
+        "android-non-updatable",
+    ],
+
+    hidden_api: {
+
+        // The following packages contain classes from other modules on the
+        // bootclasspath. That means that the hidden API flags for this module
+        // has to explicitly list every single class this module provides in
+        // that package to differentiate them from the classes provided by other
+        // modules. That can include private classes that are not part of the
+        // API.
+        split_packages: [
+            "android.uwb",
+        ],
+
+        // The following packages and all their subpackages currently only
+        // contain classes from this bootclasspath_fragment. Listing a package
+        // here won't prevent other bootclasspath modules from adding classes in
+        // any of those packages but it will prevent them from adding those
+        // classes into an API surface, e.g. public, system, etc.. Doing so will
+        // result in a build failure due to inconsistent flags.
+        package_prefixes: [
+            "com.android.x",
+        ],
+    },
+}
+
+systemserverclasspath_fragment {
+    name: "com.android.uwb-systemserverclasspath-fragment",
+    standalone_contents: ["service-uwb"],
+    apex_available: ["com.android.uwb"],
+}
diff --git a/apex/apex_manifest.json b/apex/apex_manifest.json
index 80f5ac4..88baf79 100644
--- a/apex/apex_manifest.json
+++ b/apex/apex_manifest.json
@@ -2,3 +2,4 @@
   "name": "com.android.uwb",
   "version": 339990000
 }
+
diff --git a/apex/com.android.uwb.pk8 b/apex/com.android.uwb.pk8
new file mode 100644
index 0000000..eceec74
--- /dev/null
+++ b/apex/com.android.uwb.pk8
Binary files differ
diff --git a/apex/com.android.uwb.x509.pem b/apex/com.android.uwb.x509.pem
new file mode 100644
index 0000000..62a19f8
--- /dev/null
+++ b/apex/com.android.uwb.x509.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFwzCCA6sCFFSzzpfxJOLnbv0hhqBGrMrkyXU0MA0GCSqGSIb3DQEBCwUAMIGc
+MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
+bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDEi
+MCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTEYMBYGA1UEAwwPY29t
+LmFuZHJvaWQudXdiMCAXDTIxMDcyOTIzMDEzMFoYDzQ3NTkwNjI1MjMwMTMwWjCB
+nDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1v
+dW50YWluIFZpZXcxEDAOBgNVBAoMB0FuZHJvaWQxEDAOBgNVBAsMB0FuZHJvaWQx
+IjAgBgkqhkiG9w0BCQEWE2FuZHJvaWRAYW5kcm9pZC5jb20xGDAWBgNVBAMMD2Nv
+bS5hbmRyb2lkLnV3YjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALVU
+CPcKc+MtkSiwNneTCfs2osOwo9lN3+31cEmjH3ZKu1P0vSmZ/uvk0YmpUCeDWJwi
+IOUCdAphKUwRpM/aVNJM9WVsD50IY4xdh22JTwGbgkf2DQpzAyWnVxd7yqq6EmZn
+bEPgkHkPBwsdS5idUpyuJoAcINUYzbnknc6Pj6U3WyHZGPB/+RQavqIdJUoBrPBR
+1Pz4Cx3uNdJci8ITHVPmg94X0aAKuSHHp06u5xCQP/veIu/WQQRzGQWhhG/cUmke
+0eM2jYG79c9knZV+OXbs549uSvMVsQWIuXApt255NzjStqbyQwcXBzJcwB7J1n84
+Co+y44hGPzgbKzHaPMR9XF+OA9NsPPZK4ePkLQMZsTCkp85SZQCzthzmt+f1Fft8
+uaf+2Lt+mWd5K8qRFvHr985806CjWmGHAWeoFwIVWwp1phghd7ceDjrR+fPL8v4l
+iF1GY6PlapNUYreNMVcXZWtFBLym6IyiZ0tJKCQ/LXencbj35yF1qs9fKTLHv5Qi
+5R0I+qAP3BgZBsSYiwbhBY+lsfJLPUqQtwd4Iq97xSb8YTtQ3ka6yOsRE5aLh/l8
+Zr5j9Hh8CUKHa2WKpAOym9ivrCZTDZr1KflbtAmZ+oHaptW1w3+eVGucW1oVzVII
+oegJ5Hl+x1V8mLuda2i1kzK4woFajL+9uCMykW2PAgMBAAEwDQYJKoZIhvcNAQEL
+BQADggIBAIzU5w7gA8ASaT8ZsHuX7n4I4HbUKvER/dliT7UpYOwPSMz1g7wHVJWE
+SsFOxOYzSqakbXQzSA6lkNd6jNBsAJkfxCmzcNK+ipAJkx8EMTIWmmkhemrZ+dD9
+CxXEVy9qfzQGAvxGmWgsjkNwoSJJFu1+36bYoLd2xdRwlV6nZ+32+0UqlCtiZNI8
+ZmwuKaykphc0Tbgg9ysxfvtM30zjtIAvFUtsyunOubtxyfauuF4zX+uDVL7t7bJM
+dySLWWQy2YsKAlgz9HZmuImZ3OSNiib22I2SkKPNBgpH7BShGADNdSJoW/2SFSN9
+5fYlYMNFYapxqFY1ElfvCyTW120ZkmODmbk3BBSlP55tPUZPP+9gp11HTPgpsx14
+YYBs4SfwOHP4gWF3T18CAIVKogBikBUhJoV96jsyKytqcP+lITeJfQLGaWXDlXDJ
+PfTwcYkoBC4DO1JEgcuoBhUJn9HSf+momi2NjTC9X7UQIrlpHEh47yZ8kHnG8sOJ
+qWjNVJ5xQOHrCkI3xwhrpehUEuzpeunl/3dgAruATINq+cOw1OZNclQA36XEkG4k
+nUy2i1hX7mJtLw2LMXjomcXJj+mtp0h0B+viTipE2t5qfUyy04Oj3kXHaCZ/Zl3c
+5RC4eUScb1ovlGeNk54zAMjTqG8TtaPNdh/YTNqyljCKHXN88Lzl
+-----END CERTIFICATE-----
diff --git a/framework/Android.bp b/framework/Android.bp
new file mode 100644
index 0000000..a2b67b3
--- /dev/null
+++ b/framework/Android.bp
@@ -0,0 +1,152 @@
+// 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.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+    name: "uwb-module-sdk-version-defaults",
+    min_sdk_version: "Tiramisu",
+    target_sdk_version: "Tiramisu",
+}
+
+filegroup {
+    name: "framework-uwb-updatable-exported-aidl-sources",
+    srcs: ["aidl-export/**/*.aidl"],
+    path: "aidl-export",
+    visibility: ["//visibility:private"],
+}
+
+filegroup {
+    name: "framework-uwb-updatable-java-sources",
+    srcs: [
+        "java/**/*.java",
+        "java/**/*.aidl",
+    ],
+    path: "java",
+    visibility: ["//visibility:private"],
+}
+
+filegroup {
+    name: "framework-uwb-updatable-sources",
+    srcs: [
+        ":framework-uwb-updatable-java-sources",
+        ":framework-uwb-updatable-exported-aidl-sources",
+    ],
+    visibility: [
+        "//frameworks/base",
+    ],
+}
+
+
+// defaults shared between `framework-uwb` & `framework-uwb-pre-jarjar`
+// java_sdk_library `framework-uwb` needs sources to generate stubs, so it cannot reuse
+// `framework-uwb-pre-jarjar`
+java_defaults {
+    name: "framework-uwb-defaults",
+    defaults: ["uwb-module-sdk-version-defaults"],
+    static_libs : [
+        "modules-utils-preconditions",
+    ],
+    libs: [
+        "unsupportedappusage", // for android.compat.annotation.UnsupportedAppUsage
+    ],
+    srcs: [
+        ":framework-uwb-updatable-sources",
+    ],
+}
+
+// uwb-service needs pre-jarjared version of framework-uwb so it can reference copied utility
+// classes before they are renamed.
+java_library {
+    name: "framework-uwb-pre-jarjar",
+    defaults: ["framework-uwb-defaults"],
+    sdk_version: "module_Tiramisu",
+    libs: ["framework-annotations-lib",],
+    // java_api_finder must accompany `srcs` (`srcs` defined in `framework-uwb-defaults`)
+    plugins: ["java_api_finder"],
+    installable: false,
+}
+
+// post-jarjar version of framework-uwb
+java_sdk_library {
+    name: "framework-uwb",
+    defaults: [
+        "framework-module-defaults",
+        "framework-uwb-defaults",
+    ],
+    jarjar_rules: ":uwb-jarjar-rules",
+
+    installable: true,
+    optimize: {
+        enabled: false
+    },
+    hostdex: true, // for hiddenapi check
+
+    impl_library_visibility: [
+        "//cts/tests/uwb:__subpackages__",
+        "//external/sl4a/Common:__subpackages__",
+        "//packages/modules/Uwb:__subpackages__",
+    ],
+
+    apex_available: [
+        "com.android.uwb",
+    ],
+    permitted_packages: [
+        "android.uwb",
+        // Created by jarjar rules.
+        "com.android.x.uwb",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+    },
+}
+
+// defaults for tests that need to build against framework-uwb's @hide APIs
+java_defaults {
+    name: "framework-uwb-test-defaults",
+    sdk_version: "module_Tiramisu",
+    libs: [
+        "framework-uwb.impl",
+    ],
+    defaults_visibility: [
+        "//packages/modules/Uwb/framework/tests:__subpackages__",
+        "//packages/modules/Uwb/service/tests:__subpackages__",
+    ],
+}
+
+// TODO(b/186585880): Fix all @hide dependencies.
+// defaults for CTS tests that need to build against framework-uwb's @hide APIs
+java_defaults {
+    name: "framework-uwb-cts-defaults",
+    sdk_version: "core_current",
+    libs: [
+        // order matters: classes in framework-uwb are resolved before framework, meaning
+        // @hide APIs in framework-uwb are resolved before @SystemApi stubs in framework
+        "framework-uwb.impl",
+        "framework",
+
+        // if sdk_version="" this gets automatically included, but here we need to add manually.
+        "framework-res",
+    ],
+    defaults_visibility: [
+        "//cts/tests/uwb:__subpackages__"
+    ],
+}
+
+filegroup {
+    name: "uwb-jarjar-rules",
+    srcs: ["jarjar-rules.txt"],
+}
diff --git a/framework/api/current.txt b/framework/api/current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/framework/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
new file mode 100644
index 0000000..0f0b031
--- /dev/null
+++ b/framework/api/module-lib-current.txt
@@ -0,0 +1,9 @@
+// Signature format: 2.0
+package android.uwb {
+
+  public class UwbFrameworkInitializer {
+    method public static void registerServiceWrappers();
+  }
+
+}
+
diff --git a/framework/api/module-lib-removed.txt b/framework/api/module-lib-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/framework/api/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/framework/api/removed.txt b/framework/api/removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/framework/api/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
new file mode 100644
index 0000000..bc51295
--- /dev/null
+++ b/framework/api/system-current.txt
@@ -0,0 +1,230 @@
+// Signature format: 2.0
+package android.uwb {
+
+  public final class AngleMeasurement implements android.os.Parcelable {
+    ctor public AngleMeasurement(@FloatRange(from=-3.141592653589793, to=3.141592653589793) double, @FloatRange(from=0.0, to=3.141592653589793) double, @FloatRange(from=0.0, to=1.0) double);
+    method public int describeContents();
+    method @FloatRange(from=0.0, to=1.0) public double getConfidenceLevel();
+    method @FloatRange(from=0.0, to=3.141592653589793) public double getErrorRadians();
+    method @FloatRange(from=-3.141592653589793, to=3.141592653589793) public double getRadians();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.uwb.AngleMeasurement> CREATOR;
+  }
+
+  public final class AngleOfArrivalMeasurement implements android.os.Parcelable {
+    method public int describeContents();
+    method @Nullable public android.uwb.AngleMeasurement getAltitude();
+    method @NonNull public android.uwb.AngleMeasurement getAzimuth();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.uwb.AngleOfArrivalMeasurement> CREATOR;
+  }
+
+  public static final class AngleOfArrivalMeasurement.Builder {
+    ctor public AngleOfArrivalMeasurement.Builder(@NonNull android.uwb.AngleMeasurement);
+    method @NonNull public android.uwb.AngleOfArrivalMeasurement build();
+    method @NonNull public android.uwb.AngleOfArrivalMeasurement.Builder setAltitude(@NonNull android.uwb.AngleMeasurement);
+  }
+
+  public final class DistanceMeasurement implements android.os.Parcelable {
+    method public int describeContents();
+    method @FloatRange(from=0.0, to=1.0) public double getConfidenceLevel();
+    method @FloatRange(from=0.0) public double getErrorMeters();
+    method public double getMeters();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.uwb.DistanceMeasurement> CREATOR;
+  }
+
+  public static final class DistanceMeasurement.Builder {
+    ctor public DistanceMeasurement.Builder();
+    method @NonNull public android.uwb.DistanceMeasurement build();
+    method @NonNull public android.uwb.DistanceMeasurement.Builder setConfidenceLevel(@FloatRange(from=0.0, to=1.0) double);
+    method @NonNull public android.uwb.DistanceMeasurement.Builder setErrorMeters(@FloatRange(from=0.0) double);
+    method @NonNull public android.uwb.DistanceMeasurement.Builder setMeters(double);
+  }
+
+  public final class RangingMeasurement implements android.os.Parcelable {
+    method public int describeContents();
+    method @Nullable public android.uwb.AngleOfArrivalMeasurement getAngleOfArrivalMeasurement();
+    method @Nullable public android.uwb.AngleOfArrivalMeasurement getDestinationAngleOfArrivalMeasurement();
+    method @Nullable public android.uwb.DistanceMeasurement getDistanceMeasurement();
+    method public long getElapsedRealtimeNanos();
+    method public int getLineOfSight();
+    method public int getMeasurementFocus();
+    method @NonNull public android.uwb.UwbAddress getRemoteDeviceAddress();
+    method @IntRange(from=android.uwb.RangingMeasurement.RSSI_UNKNOWN, to=android.uwb.RangingMeasurement.RSSI_MAX) public int getRssiDbm();
+    method public int getStatus();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.uwb.RangingMeasurement> CREATOR;
+    field public static final int LOS = 0; // 0x0
+    field public static final int LOS_UNDETERMINED = 255; // 0xff
+    field public static final int MEASUREMENT_FOCUS_ANGLE_OF_ARRIVAL_AZIMUTH = 2; // 0x2
+    field public static final int MEASUREMENT_FOCUS_ANGLE_OF_ARRIVAL_ELEVATION = 3; // 0x3
+    field public static final int MEASUREMENT_FOCUS_NONE = 0; // 0x0
+    field public static final int MEASUREMENT_FOCUS_RANGE = 1; // 0x1
+    field public static final int NLOS = 1; // 0x1
+    field public static final int RANGING_STATUS_FAILURE_OUT_OF_RANGE = 1; // 0x1
+    field public static final int RANGING_STATUS_FAILURE_UNKNOWN_ERROR = -1; // 0xffffffff
+    field public static final int RANGING_STATUS_SUCCESS = 0; // 0x0
+    field public static final int RSSI_MAX = -1; // 0xffffffff
+    field public static final int RSSI_MIN = -127; // 0xffffff81
+    field public static final int RSSI_UNKNOWN = -128; // 0xffffff80
+  }
+
+  public static final class RangingMeasurement.Builder {
+    ctor public RangingMeasurement.Builder();
+    method @NonNull public android.uwb.RangingMeasurement build();
+    method @NonNull public android.uwb.RangingMeasurement.Builder setAngleOfArrivalMeasurement(@NonNull android.uwb.AngleOfArrivalMeasurement);
+    method @NonNull public android.uwb.RangingMeasurement.Builder setDestinationAngleOfArrivalMeasurement(@NonNull android.uwb.AngleOfArrivalMeasurement);
+    method @NonNull public android.uwb.RangingMeasurement.Builder setDistanceMeasurement(@NonNull android.uwb.DistanceMeasurement);
+    method @NonNull public android.uwb.RangingMeasurement.Builder setElapsedRealtimeNanos(long);
+    method @NonNull public android.uwb.RangingMeasurement.Builder setLineOfSight(int);
+    method @NonNull public android.uwb.RangingMeasurement.Builder setMeasurementFocus(int);
+    method @NonNull public android.uwb.RangingMeasurement.Builder setRemoteDeviceAddress(@NonNull android.uwb.UwbAddress);
+    method @NonNull public android.uwb.RangingMeasurement.Builder setRssiDbm(@IntRange(from=android.uwb.RangingMeasurement.RSSI_UNKNOWN, to=android.uwb.RangingMeasurement.RSSI_MAX) int);
+    method @NonNull public android.uwb.RangingMeasurement.Builder setStatus(int);
+  }
+
+  public final class RangingReport implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public java.util.List<android.uwb.RangingMeasurement> getMeasurements();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.uwb.RangingReport> CREATOR;
+  }
+
+  public static final class RangingReport.Builder {
+    ctor public RangingReport.Builder();
+    method @NonNull public android.uwb.RangingReport.Builder addMeasurement(@NonNull android.uwb.RangingMeasurement);
+    method @NonNull public android.uwb.RangingReport.Builder addMeasurements(@NonNull java.util.List<android.uwb.RangingMeasurement>);
+    method @NonNull public android.uwb.RangingReport build();
+  }
+
+  public final class RangingSession implements java.lang.AutoCloseable {
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public void addControlee(@NonNull android.os.PersistableBundle);
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public void close();
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public void pause(@NonNull android.os.PersistableBundle);
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public void reconfigure(@NonNull android.os.PersistableBundle);
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public void removeControlee(@NonNull android.os.PersistableBundle);
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public void resume(@NonNull android.os.PersistableBundle);
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public void sendData(@NonNull android.uwb.UwbAddress, @NonNull android.os.PersistableBundle, @NonNull byte[]);
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public void start(@NonNull android.os.PersistableBundle);
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public void stop();
+  }
+
+  public static interface RangingSession.Callback {
+    method public void onClosed(int, @NonNull android.os.PersistableBundle);
+    method public default void onControleeAddFailed(int, @NonNull android.os.PersistableBundle);
+    method public default void onControleeAdded(@NonNull android.os.PersistableBundle);
+    method public default void onControleeRemoveFailed(int, @NonNull android.os.PersistableBundle);
+    method public default void onControleeRemoved(@NonNull android.os.PersistableBundle);
+    method public default void onDataReceiveFailed(@NonNull android.uwb.UwbAddress, int, @NonNull android.os.PersistableBundle);
+    method public default void onDataReceived(@NonNull android.uwb.UwbAddress, @NonNull android.os.PersistableBundle, @NonNull byte[]);
+    method public default void onDataSendFailed(@NonNull android.uwb.UwbAddress, int, @NonNull android.os.PersistableBundle);
+    method public default void onDataSent(@NonNull android.uwb.UwbAddress, @NonNull android.os.PersistableBundle);
+    method public void onOpenFailed(int, @NonNull android.os.PersistableBundle);
+    method public void onOpened(@NonNull android.uwb.RangingSession);
+    method public default void onPauseFailed(int, @NonNull android.os.PersistableBundle);
+    method public default void onPaused(@NonNull android.os.PersistableBundle);
+    method public void onReconfigureFailed(int, @NonNull android.os.PersistableBundle);
+    method public void onReconfigured(@NonNull android.os.PersistableBundle);
+    method public void onReportReceived(@NonNull android.uwb.RangingReport);
+    method public default void onResumeFailed(int, @NonNull android.os.PersistableBundle);
+    method public default void onResumed(@NonNull android.os.PersistableBundle);
+    method public default void onServiceConnected(@NonNull android.os.PersistableBundle);
+    method public default void onServiceDiscovered(@NonNull android.os.PersistableBundle);
+    method public void onStartFailed(int, @NonNull android.os.PersistableBundle);
+    method public void onStarted(@NonNull android.os.PersistableBundle);
+    method public void onStopFailed(int, @NonNull android.os.PersistableBundle);
+    method public void onStopped(int, @NonNull android.os.PersistableBundle);
+    field public static final int CONTROLEE_FAILURE_REASON_MAX_CONTROLEE_REACHED = 0; // 0x0
+    field public static final int DATA_FAILURE_REASON_DATA_SIZE_TOO_LARGE = 10; // 0xa
+    field public static final int REASON_BAD_PARAMETERS = 3; // 0x3
+    field public static final int REASON_GENERIC_ERROR = 4; // 0x4
+    field public static final int REASON_LOCAL_REQUEST = 1; // 0x1
+    field public static final int REASON_MAX_RR_RETRY_REACHED = 9; // 0x9
+    field public static final int REASON_MAX_SESSIONS_REACHED = 5; // 0x5
+    field public static final int REASON_PROTOCOL_SPECIFIC_ERROR = 7; // 0x7
+    field public static final int REASON_REMOTE_REQUEST = 2; // 0x2
+    field public static final int REASON_SERVICE_CONNECTION_FAILURE = 11; // 0xb
+    field public static final int REASON_SERVICE_DISCOVERY_FAILURE = 10; // 0xa
+    field public static final int REASON_SE_INTERACTION_FAILURE = 13; // 0xd
+    field public static final int REASON_SE_NOT_SUPPORTED = 12; // 0xc
+    field public static final int REASON_SYSTEM_POLICY = 6; // 0x6
+    field public static final int REASON_UNKNOWN = 0; // 0x0
+  }
+
+  public final class UwbAddress implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public static android.uwb.UwbAddress fromBytes(@NonNull byte[]);
+    method public int size();
+    method @NonNull public byte[] toBytes();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.uwb.UwbAddress> CREATOR;
+    field public static final int EXTENDED_ADDRESS_BYTE_LENGTH = 8; // 0x8
+    field public static final int SHORT_ADDRESS_BYTE_LENGTH = 2; // 0x2
+  }
+
+  public final class UwbManager {
+    method @NonNull @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public android.os.PersistableBundle addServiceProfile(@NonNull android.os.PersistableBundle);
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public long elapsedRealtimeResolutionNanos();
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public long elapsedRealtimeResolutionNanos(@NonNull String);
+    method public int getAdapterState();
+    method @NonNull @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public android.os.PersistableBundle getAdfCertificateInfo(@NonNull android.os.PersistableBundle);
+    method @NonNull @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public android.os.PersistableBundle getAdfProvisioningAuthorities(@NonNull android.os.PersistableBundle);
+    method @NonNull @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public android.os.PersistableBundle getAllServiceProfiles();
+    method @NonNull @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public java.util.List<android.os.PersistableBundle> getChipInfos();
+    method @NonNull @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public String getDefaultChipId();
+    method @NonNull @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public android.os.PersistableBundle getSpecificationInfo();
+    method @NonNull @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public android.os.PersistableBundle getSpecificationInfo(@NonNull String);
+    method public boolean isUwbEnabled();
+    method @NonNull @RequiresPermission(allOf={android.Manifest.permission.UWB_PRIVILEGED, android.Manifest.permission.UWB_RANGING}) public android.os.CancellationSignal openRangingSession(@NonNull android.os.PersistableBundle, @NonNull java.util.concurrent.Executor, @NonNull android.uwb.RangingSession.Callback);
+    method @NonNull @RequiresPermission(allOf={android.Manifest.permission.UWB_PRIVILEGED, android.Manifest.permission.UWB_RANGING}) public android.os.CancellationSignal openRangingSession(@NonNull android.os.PersistableBundle, @NonNull java.util.concurrent.Executor, @NonNull android.uwb.RangingSession.Callback, @NonNull String);
+    method public void provisionProfileAdfByScript(@NonNull android.os.PersistableBundle, @NonNull java.util.concurrent.Executor, @NonNull android.uwb.UwbManager.AdfProvisionStateCallback);
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public void registerAdapterStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.uwb.UwbManager.AdapterStateCallback);
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public void registerUwbVendorUciCallback(@NonNull java.util.concurrent.Executor, @NonNull android.uwb.UwbManager.UwbVendorUciCallback);
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public int removeProfileAdf(@NonNull android.os.PersistableBundle);
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public int removeServiceProfile(@NonNull android.os.PersistableBundle);
+    method @NonNull @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public int sendVendorUciMessage(@IntRange(from=9, to=15) int, int, @NonNull byte[]);
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public void setUwbEnabled(boolean);
+    method @RequiresPermission(android.Manifest.permission.UWB_PRIVILEGED) public void unregisterAdapterStateCallback(@NonNull android.uwb.UwbManager.AdapterStateCallback);
+    method public void unregisterUwbVendorUciCallback(@NonNull android.uwb.UwbManager.UwbVendorUciCallback);
+    field public static final int REMOVE_PROFILE_ADF_ERROR_INTERNAL = 2; // 0x2
+    field public static final int REMOVE_PROFILE_ADF_ERROR_UNKNOWN_SERVICE = 1; // 0x1
+    field public static final int REMOVE_PROFILE_ADF_SUCCESS = 0; // 0x0
+    field public static final int REMOVE_SERVICE_PROFILE_ERROR_INTERNAL = 2; // 0x2
+    field public static final int REMOVE_SERVICE_PROFILE_ERROR_UNKNOWN_SERVICE = 1; // 0x1
+    field public static final int REMOVE_SERVICE_PROFILE_SUCCESS = 0; // 0x0
+    field public static final int SEND_VENDOR_UCI_ERROR_HW = 1; // 0x1
+    field public static final int SEND_VENDOR_UCI_ERROR_INVALID_ARGS = 3; // 0x3
+    field public static final int SEND_VENDOR_UCI_ERROR_INVALID_GID = 4; // 0x4
+    field public static final int SEND_VENDOR_UCI_ERROR_OFF = 2; // 0x2
+    field public static final int SEND_VENDOR_UCI_SUCCESS = 0; // 0x0
+  }
+
+  public static interface UwbManager.AdapterStateCallback {
+    method public void onStateChanged(int, int);
+    field public static final int STATE_CHANGED_REASON_ALL_SESSIONS_CLOSED = 1; // 0x1
+    field public static final int STATE_CHANGED_REASON_ERROR_UNKNOWN = 4; // 0x4
+    field public static final int STATE_CHANGED_REASON_SESSION_STARTED = 0; // 0x0
+    field public static final int STATE_CHANGED_REASON_SYSTEM_BOOT = 3; // 0x3
+    field public static final int STATE_CHANGED_REASON_SYSTEM_POLICY = 2; // 0x2
+    field public static final int STATE_DISABLED = 0; // 0x0
+    field public static final int STATE_ENABLED_ACTIVE = 2; // 0x2
+    field public static final int STATE_ENABLED_INACTIVE = 1; // 0x1
+  }
+
+  public abstract static class UwbManager.AdfProvisionStateCallback {
+    ctor public UwbManager.AdfProvisionStateCallback();
+    method public abstract void onProfileAdfsProvisionFailed(int, @NonNull android.os.PersistableBundle);
+    method public abstract void onProfileAdfsProvisioned(@NonNull android.os.PersistableBundle);
+    field public static final int REASON_INVALID_OID = 1; // 0x1
+    field public static final int REASON_SE_FAILURE = 2; // 0x2
+    field public static final int REASON_UNKNOWN = 3; // 0x3
+  }
+
+  public static interface UwbManager.UwbVendorUciCallback {
+    method public void onVendorUciNotification(@IntRange(from=9, to=15) int, int, @NonNull byte[]);
+    method public void onVendorUciResponse(@IntRange(from=9, to=15) int, int, @NonNull byte[]);
+  }
+
+}
+
diff --git a/framework/api/system-removed.txt b/framework/api/system-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/framework/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/framework/jarjar-rules.txt b/framework/jarjar-rules.txt
new file mode 100644
index 0000000..1ef2349
--- /dev/null
+++ b/framework/jarjar-rules.txt
@@ -0,0 +1,13 @@
+## used by service-uwb ##
+# Statically included annotations.
+rule androidx.annotation.** com.android.x.uwb.@0
+# Statically included module utils.
+rule com.android.modules.utils.** com.android.x.uwb.@0
+# Statically included HAL stubs.
+rule android.hardware.uwb.** com.android.x.uwb.@0
+# Statically included UWB support lib and guava lib.
+rule com.google.** com.android.x.uwb.@0
+# Included by support lib.
+rule com.android.internal.util.** com.android.x.uwb.@0
+
+## used by both framework-uwb and service-uwb ##
diff --git a/framework/java/android/uwb/AdapterState.aidl b/framework/java/android/uwb/AdapterState.aidl
new file mode 100644
index 0000000..991f64a
--- /dev/null
+++ b/framework/java/android/uwb/AdapterState.aidl
@@ -0,0 +1,38 @@
+/*
+ * Copyright 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.
+ */
+
+package android.uwb;
+
+/**
+ * @hide
+ */
+@Backing(type="int")
+enum AdapterState {
+ /**
+   * The state when UWB is disabled.
+   */
+  STATE_DISABLED,
+
+  /**
+   * The state when UWB is enabled but has no active sessions.
+   */
+  STATE_ENABLED_INACTIVE,
+
+  /**
+   * The state when UWB is enabled and has active sessions.
+   */
+  STATE_ENABLED_ACTIVE,
+}
\ No newline at end of file
diff --git a/framework/java/android/uwb/AdapterStateListener.java b/framework/java/android/uwb/AdapterStateListener.java
new file mode 100644
index 0000000..7e82cc6
--- /dev/null
+++ b/framework/java/android/uwb/AdapterStateListener.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import android.annotation.NonNull;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.uwb.UwbManager.AdapterStateCallback;
+import android.uwb.UwbManager.AdapterStateCallback.State;
+import android.uwb.UwbManager.AdapterStateCallback.StateChangedReason;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ */
+public class AdapterStateListener extends IUwbAdapterStateCallbacks.Stub {
+    private static final String TAG = "Uwb.StateListener";
+
+    private final IUwbAdapter mAdapter;
+    private boolean mIsRegistered = false;
+
+    private final Map<AdapterStateCallback, Executor> mCallbackMap = new HashMap<>();
+
+    @StateChangedReason
+    private int mAdapterStateChangeReason = AdapterStateCallback.STATE_CHANGED_REASON_ERROR_UNKNOWN;
+    @State
+    private int mAdapterState = AdapterStateCallback.STATE_DISABLED;
+
+    public AdapterStateListener(@NonNull IUwbAdapter adapter) {
+        mAdapter = adapter;
+    }
+
+    /**
+     * Register an {@link AdapterStateCallback} with this {@link AdapterStateListener}
+     *
+     * @param executor an {@link Executor} to execute given callback
+     * @param callback user implementation of the {@link AdapterStateCallback}
+     */
+    public void register(@NonNull Executor executor, @NonNull AdapterStateCallback callback) {
+        synchronized (this) {
+            if (mCallbackMap.containsKey(callback)) {
+                return;
+            }
+
+            mCallbackMap.put(callback, executor);
+
+            if (!mIsRegistered) {
+                try {
+                    mAdapter.registerAdapterStateCallbacks(this);
+                    mIsRegistered = true;
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Failed to register adapter state callback");
+                    throw e.rethrowFromSystemServer();
+                }
+            } else {
+                sendCurrentState(callback);
+            }
+        }
+    }
+
+    /**
+     * Unregister the specified {@link AdapterStateCallback}
+     *
+     * @param callback user implementation of the {@link AdapterStateCallback}
+     */
+    public void unregister(@NonNull AdapterStateCallback callback) {
+        synchronized (this) {
+            if (!mCallbackMap.containsKey(callback)) {
+                return;
+            }
+
+            mCallbackMap.remove(callback);
+
+            if (mCallbackMap.isEmpty() && mIsRegistered) {
+                try {
+                    mAdapter.unregisterAdapterStateCallbacks(this);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Failed to unregister AdapterStateCallback with service");
+                    throw e.rethrowFromSystemServer();
+                }
+                mIsRegistered = false;
+            }
+        }
+    }
+
+    /**
+     * Sets the adapter enabled state
+     *
+     * @param isEnabled value of new adapter state
+     */
+    public void setEnabled(boolean isEnabled) {
+        synchronized (this) {
+            try {
+                mAdapter.setEnabled(isEnabled);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed to set adapter state");
+                throw e.rethrowFromSystemServer();
+            }
+
+        }
+    }
+
+    /**
+     * Gets the adapter enabled state
+     *
+     * @return integer representing adapter enabled state
+     */
+    public int getAdapterState() {
+        synchronized (this) {
+            try {
+                return mAdapter.getAdapterState();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed to get adapter state");
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    private void sendCurrentState(@NonNull AdapterStateCallback callback) {
+        synchronized (this) {
+            Executor executor = mCallbackMap.get(callback);
+
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                executor.execute(() -> callback.onStateChanged(
+                        mAdapterState, mAdapterStateChangeReason));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+
+    @Override
+    public void onAdapterStateChanged(int state, int reason) {
+        synchronized (this) {
+            @StateChangedReason int localReason =
+                    convertToStateChangedReason(reason);
+            @State int localState = convertToState(state);
+            mAdapterStateChangeReason = localReason;
+            mAdapterState = localState;
+            for (AdapterStateCallback cb : mCallbackMap.keySet()) {
+                sendCurrentState(cb);
+            }
+        }
+    }
+
+    private static @StateChangedReason int convertToStateChangedReason(
+            @StateChangeReason int reason) {
+        switch (reason) {
+            case StateChangeReason.ALL_SESSIONS_CLOSED:
+                return AdapterStateCallback.STATE_CHANGED_REASON_ALL_SESSIONS_CLOSED;
+
+            case StateChangeReason.SESSION_STARTED:
+                return AdapterStateCallback.STATE_CHANGED_REASON_SESSION_STARTED;
+
+            case StateChangeReason.SYSTEM_POLICY:
+                return AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_POLICY;
+
+            case StateChangeReason.SYSTEM_BOOT:
+                return AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_BOOT;
+
+            case StateChangeReason.UNKNOWN:
+            default:
+                return AdapterStateCallback.STATE_CHANGED_REASON_ERROR_UNKNOWN;
+        }
+    }
+
+    private static @State int convertToState(@AdapterState int state) {
+        switch (state) {
+            case AdapterState.STATE_ENABLED_INACTIVE:
+                return AdapterStateCallback.STATE_ENABLED_INACTIVE;
+
+            case AdapterState.STATE_ENABLED_ACTIVE:
+                return AdapterStateCallback.STATE_ENABLED_ACTIVE;
+
+            case AdapterState.STATE_DISABLED:
+            default:
+                return AdapterStateCallback.STATE_DISABLED;
+        }
+    }
+}
diff --git a/framework/java/android/uwb/AngleMeasurement.java b/framework/java/android/uwb/AngleMeasurement.java
new file mode 100644
index 0000000..726f750
--- /dev/null
+++ b/framework/java/android/uwb/AngleMeasurement.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Angle measurement
+ *
+ * <p>The actual angle is interpreted as:
+ *   {@link #getRadians()} +/- {@link #getErrorRadians()} ()} at {@link #getConfidenceLevel()}
+ *
+ * @hide
+ */
+@SystemApi
+public final class AngleMeasurement implements Parcelable {
+    private final double mRadians;
+    private final double mErrorRadians;
+    private final double mConfidenceLevel;
+
+    /**
+     * Constructs a new {@link AngleMeasurement} object
+     *
+     * @param radians the angle in radians
+     * @param errorRadians the error of the angle measurement in radians
+     * @param confidenceLevel confidence level of the angle measurement
+     *
+     * @throws IllegalArgumentException if the radians, errorRadians, or confidenceLevel is out of
+     *                                  allowed range
+     */
+    public AngleMeasurement(
+            @FloatRange(from = -Math.PI, to = +Math.PI) double radians,
+            @FloatRange(from = 0.0, to = +Math.PI) double errorRadians,
+            @FloatRange(from = 0.0, to = 1.0) double confidenceLevel) {
+        if (radians < -Math.PI || radians > Math.PI) {
+            throw new IllegalArgumentException("Invalid radians: " + radians);
+        }
+        mRadians = radians;
+
+        if (errorRadians < 0.0 || errorRadians > Math.PI) {
+            throw new IllegalArgumentException("Invalid error radians: " + errorRadians);
+        }
+        mErrorRadians = errorRadians;
+
+        if (confidenceLevel < 0.0 || confidenceLevel > 1.0) {
+            throw new IllegalArgumentException("Invalid confidence level: " + confidenceLevel);
+        }
+        mConfidenceLevel = confidenceLevel;
+    }
+
+    /**
+     * Angle measurement in radians
+     *
+     * @return angle in radians
+     */
+    @FloatRange(from = -Math.PI, to = +Math.PI)
+    public double getRadians() {
+        return mRadians;
+    }
+
+    /**
+     * Error of angle measurement in radians
+     *
+     * <p>Must be a positive value
+     *
+     * @return angle measurement error in radians
+     */
+    @FloatRange(from = 0.0, to = +Math.PI)
+    public double getErrorRadians() {
+        return mErrorRadians;
+    }
+
+    /**
+     * Angle measurement confidence level expressed as a value between
+     * 0.0 to 1.0.
+     *
+     * <p>A value of 0.0 indicates there is no confidence in the measurement. A value of 1.0
+     * indicates there is maximum confidence in the measurement.
+     *
+     * @return the confidence level of the angle measurement
+     */
+    @FloatRange(from = 0.0, to = 1.0)
+    public double getConfidenceLevel() {
+        return mConfidenceLevel;
+    }
+
+    /**
+     * @hide
+    */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof AngleMeasurement) {
+            AngleMeasurement other = (AngleMeasurement) obj;
+            return mRadians == other.getRadians()
+                    && mErrorRadians == other.getErrorRadians()
+                    && mConfidenceLevel == other.getConfidenceLevel();
+        }
+        return false;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mRadians, mErrorRadians, mConfidenceLevel);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeDouble(mRadians);
+        dest.writeDouble(mErrorRadians);
+        dest.writeDouble(mConfidenceLevel);
+    }
+
+    public static final @android.annotation.NonNull Creator<AngleMeasurement> CREATOR =
+            new Creator<AngleMeasurement>() {
+                @Override
+                public AngleMeasurement createFromParcel(Parcel in) {
+                    return new AngleMeasurement(in.readDouble(), in.readDouble(), in.readDouble());
+                }
+
+                @Override
+                public AngleMeasurement[] newArray(int size) {
+                    return new AngleMeasurement[size];
+                }
+    };
+
+    /** @hide **/
+    @Override
+    public String toString() {
+        return "AngleMeasurement["
+                + "radians: " + mRadians
+                + ", errorRadians: " + mErrorRadians
+                + ", confidenceLevel: " + mConfidenceLevel
+                + "]";
+    }
+}
diff --git a/framework/java/android/uwb/AngleOfArrivalMeasurement.java b/framework/java/android/uwb/AngleOfArrivalMeasurement.java
new file mode 100644
index 0000000..196579e
--- /dev/null
+++ b/framework/java/android/uwb/AngleOfArrivalMeasurement.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Represents an angle of arrival measurement between two devices using Ultra Wideband
+ *
+ * @hide
+ */
+@SystemApi
+public final class AngleOfArrivalMeasurement implements Parcelable {
+    private final AngleMeasurement mAzimuthAngleMeasurement;
+    private final AngleMeasurement mAltitudeAngleMeasurement;
+
+    private AngleOfArrivalMeasurement(@NonNull AngleMeasurement azimuthAngleMeasurement,
+            @Nullable AngleMeasurement altitudeAngleMeasurement) {
+        mAzimuthAngleMeasurement = azimuthAngleMeasurement;
+        mAltitudeAngleMeasurement = altitudeAngleMeasurement;
+    }
+
+    /**
+     * Azimuth angle measurement
+     * <p>Azimuth {@link AngleMeasurement} of remote device in horizontal coordinate system, this is
+     * the angle clockwise from the meridian when viewing above the north pole.
+     *
+     * <p>See: https://en.wikipedia.org/wiki/Horizontal_coordinate_system
+     *
+     * <p>On an Android device, azimuth north is defined as the angle perpendicular away from the
+     * back of the device when holding it in portrait mode upright.
+     *
+     * <p>Azimuth angle must be supported when Angle of Arrival is supported
+     *
+     * @return the azimuth {@link AngleMeasurement}
+     */
+    @NonNull
+    public AngleMeasurement getAzimuth() {
+        return mAzimuthAngleMeasurement;
+    }
+
+    /**
+     * Altitude angle measurement
+     * <p>Altitude {@link AngleMeasurement} of remote device in horizontal coordinate system, this
+     * is the angle above the equator when the north pole is up.
+     *
+     * <p>See: https://en.wikipedia.org/wiki/Horizontal_coordinate_system
+     *
+     * <p>On an Android device, altitude is defined as the angle vertical from ground when holding
+     * the device in portrait mode upright.
+     *
+     * @return altitude {@link AngleMeasurement} or null when this is not available
+     */
+    @Nullable
+    public AngleMeasurement getAltitude() {
+        return mAltitudeAngleMeasurement;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof AngleOfArrivalMeasurement) {
+            AngleOfArrivalMeasurement other = (AngleOfArrivalMeasurement) obj;
+            return Objects.equals(mAzimuthAngleMeasurement, other.getAzimuth())
+                    && Objects.equals(mAltitudeAngleMeasurement, other.getAltitude());
+        }
+        return false;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mAzimuthAngleMeasurement, mAltitudeAngleMeasurement);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeParcelable(mAzimuthAngleMeasurement, flags);
+        dest.writeParcelable(mAltitudeAngleMeasurement, flags);
+    }
+
+    public static final @android.annotation.NonNull Creator<AngleOfArrivalMeasurement> CREATOR =
+            new Creator<AngleOfArrivalMeasurement>() {
+                @Override
+                public AngleOfArrivalMeasurement createFromParcel(Parcel in) {
+                    Builder builder =
+                            new Builder(in.readParcelable(AngleMeasurement.class.getClassLoader()));
+
+                    builder.setAltitude(in.readParcelable(AngleMeasurement.class.getClassLoader()));
+
+                    return builder.build();
+                }
+
+                @Override
+                public AngleOfArrivalMeasurement[] newArray(int size) {
+                    return new AngleOfArrivalMeasurement[size];
+                }
+            };
+
+    /** @hide **/
+    @Override
+    public String toString() {
+        return "AngleOfArrivalMeasurement["
+                + "azimuth: " + mAzimuthAngleMeasurement
+                + ", altitude: " + mAltitudeAngleMeasurement
+                + "]";
+    }
+
+    /**
+     * Builder class for {@link AngleOfArrivalMeasurement}.
+     */
+    public static final class Builder {
+        private final AngleMeasurement mAzimuthAngleMeasurement;
+        private AngleMeasurement mAltitudeAngleMeasurement = null;
+
+        /**
+         * Constructs an {@link AngleOfArrivalMeasurement} object
+         *
+         * @param azimuthAngle the azimuth angle of the measurement
+         */
+        public Builder(@NonNull AngleMeasurement azimuthAngle) {
+            mAzimuthAngleMeasurement = azimuthAngle;
+        }
+
+        /**
+         * Set the altitude angle
+         *
+         * @param altitudeAngle altitude angle
+         */
+        @NonNull
+        public Builder setAltitude(@NonNull AngleMeasurement altitudeAngle) {
+            mAltitudeAngleMeasurement = altitudeAngle;
+            return this;
+        }
+
+        /**
+         * Build the {@link AngleOfArrivalMeasurement} object
+         */
+        @NonNull
+        public AngleOfArrivalMeasurement build() {
+            return new AngleOfArrivalMeasurement(mAzimuthAngleMeasurement,
+                    mAltitudeAngleMeasurement);
+        }
+    }
+}
diff --git a/framework/java/android/uwb/DistanceMeasurement.java b/framework/java/android/uwb/DistanceMeasurement.java
new file mode 100644
index 0000000..c2ad5e5
--- /dev/null
+++ b/framework/java/android/uwb/DistanceMeasurement.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * A data point for the distance measurement
+ *
+ * <p>The actual distance is interpreted as:
+ *   {@link #getMeters()} +/- {@link #getErrorMeters()} at {@link #getConfidenceLevel()}
+ *
+ * @hide
+ */
+@SystemApi
+public final class DistanceMeasurement implements Parcelable {
+    private final double mMeters;
+    private final double mErrorMeters;
+    private final double mConfidenceLevel;
+
+    private DistanceMeasurement(double meters, double errorMeters, double confidenceLevel) {
+        mMeters = meters;
+        mErrorMeters = errorMeters;
+        mConfidenceLevel = confidenceLevel;
+    }
+
+    /**
+     * Distance measurement in meters
+     *
+     * @return distance in meters
+     */
+    public double getMeters() {
+        return mMeters;
+    }
+
+    /**
+     * Error of distance measurement in meters
+     * <p>Must be positive
+     *
+     * @return error of distance measurement in meters
+     */
+    @FloatRange(from = 0.0)
+    public double getErrorMeters() {
+        return mErrorMeters;
+    }
+
+    /**
+     * Distance measurement confidence level expressed as a value between 0.0 to 1.0.
+     *
+     * <p>A value of 0.0 indicates no confidence in the measurement. A value of 1.0 represents
+     * maximum confidence in the measurement
+     *
+     * @return confidence level
+     */
+    @FloatRange(from = 0.0, to = 1.0)
+    public double getConfidenceLevel() {
+        return mConfidenceLevel;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof DistanceMeasurement) {
+            DistanceMeasurement other = (DistanceMeasurement) obj;
+            return mMeters == other.getMeters()
+                    && mErrorMeters == other.getErrorMeters()
+                    && mConfidenceLevel == other.getConfidenceLevel();
+        }
+        return false;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mMeters, mErrorMeters, mConfidenceLevel);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeDouble(mMeters);
+        dest.writeDouble(mErrorMeters);
+        dest.writeDouble(mConfidenceLevel);
+    }
+
+    public static final @android.annotation.NonNull Creator<DistanceMeasurement> CREATOR =
+            new Creator<DistanceMeasurement>() {
+                @Override
+                public DistanceMeasurement createFromParcel(Parcel in) {
+                    Builder builder = new Builder();
+                    builder.setMeters(in.readDouble());
+                    builder.setErrorMeters(in.readDouble());
+                    builder.setConfidenceLevel(in.readDouble());
+                    return builder.build();
+                }
+
+                @Override
+                public DistanceMeasurement[] newArray(int size) {
+                    return new DistanceMeasurement[size];
+                }
+    };
+
+    /** @hide **/
+    @Override
+    public String toString() {
+        return "DistanceMeasurement["
+                + "meters: " + mMeters
+                + ", errorMeters: " + mErrorMeters
+                + ", confidenceLevel: " + mConfidenceLevel
+                + "]";
+    }
+
+    /**
+     * Builder to get a {@link DistanceMeasurement} object.
+     */
+    public static final class Builder {
+        private double mMeters = Double.NaN;
+        private double mErrorMeters = Double.NaN;
+        private double mConfidenceLevel = Double.NaN;
+
+        /**
+         * Set the distance measurement in meters
+         *
+         * @param meters distance in meters
+         * @throws IllegalArgumentException if meters is NaN
+         */
+        @NonNull
+        public Builder setMeters(double meters) {
+            if (Double.isNaN(meters)) {
+                throw new IllegalArgumentException("meters cannot be NaN");
+            }
+            mMeters = meters;
+            return this;
+        }
+
+        /**
+         * Set the distance error in meters
+         *
+         * @param errorMeters distance error in meters
+         * @throws IllegalArgumentException if error is negative or NaN
+         */
+        @NonNull
+        public Builder setErrorMeters(@FloatRange(from = 0.0) double errorMeters) {
+            if (Double.isNaN(errorMeters) || errorMeters < 0.0) {
+                throw new IllegalArgumentException(
+                        "errorMeters must be >= 0.0 and not NaN: " + errorMeters);
+            }
+            mErrorMeters = errorMeters;
+            return this;
+        }
+
+        /**
+         * Set the confidence level
+         *
+         * @param confidenceLevel the confidence level in the distance measurement
+         * @throws IllegalArgumentException if confidence level is not in the range of [0.0, 1.0]
+         */
+        @NonNull
+        public Builder setConfidenceLevel(
+                @FloatRange(from = 0.0, to = 1.0) double confidenceLevel) {
+            if (confidenceLevel < 0.0 || confidenceLevel > 1.0) {
+                throw new IllegalArgumentException(
+                        "confidenceLevel must be in the range [0.0, 1.0]: " + confidenceLevel);
+            }
+            mConfidenceLevel = confidenceLevel;
+            return this;
+        }
+
+        /**
+         * Builds the {@link DistanceMeasurement} object
+         *
+         * @throws IllegalStateException if meters, error, or confidence are not set
+         */
+        @NonNull
+        public DistanceMeasurement build() {
+            if (Double.isNaN(mMeters)) {
+                throw new IllegalStateException("Meters cannot be NaN");
+            }
+
+            if (Double.isNaN(mErrorMeters)) {
+                throw new IllegalStateException("Error meters cannot be NaN");
+            }
+
+            if (Double.isNaN(mConfidenceLevel)) {
+                throw new IllegalStateException("Confidence level cannot be NaN");
+            }
+
+            return new DistanceMeasurement(mMeters, mErrorMeters, mConfidenceLevel);
+        }
+    }
+}
diff --git a/framework/java/android/uwb/IUwbAdapter.aidl b/framework/java/android/uwb/IUwbAdapter.aidl
new file mode 100644
index 0000000..c1ddc9a
--- /dev/null
+++ b/framework/java/android/uwb/IUwbAdapter.aidl
@@ -0,0 +1,360 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import android.content.AttributionSource;
+import android.os.PersistableBundle;
+import android.uwb.IUwbAdapterStateCallbacks;
+import android.uwb.IUwbAdfProvisionStateCallbacks;
+import android.uwb.IUwbRangingCallbacks;
+import android.uwb.SessionHandle;
+import android.uwb.UwbAddress;
+import android.uwb.IUwbVendorUciCallback;
+
+/**
+ * @hide
+ * TODO(b/211025367): Remove all the duplicate javadocs here.
+ */
+interface IUwbAdapter {
+  /*
+   * Register the callbacks used to notify the framework of events and data
+   *
+   * The provided callback's IUwbAdapterStateCallbacks#onAdapterStateChanged
+   * function must be called immediately following registration with the current
+   * state of the UWB adapter.
+   *
+   * @param callbacks callback to provide range and status updates to the framework
+   */
+  void registerAdapterStateCallbacks(in IUwbAdapterStateCallbacks adapterStateCallbacks);
+
+   /*
+    * Register the callbacks used to notify the framework of events and data
+    *
+    * The provided callback's IUwbUciVendorCallback#onVendorNotificationReceived
+    * function must be called immediately following vendorNotification received
+    *
+    * @param callbacks callback to provide Notification data updates to the framework
+    */
+   void registerVendorExtensionCallback(in IUwbVendorUciCallback callbacks);
+
+   /*
+    * Unregister the callbacks used to notify the framework of events and data
+    *
+    * Calling this function with an unregistered callback is a no-op
+    *
+    * @param callbacks callback to unregister
+    */
+   void unregisterVendorExtensionCallback(in IUwbVendorUciCallback callbacks);
+
+   /*
+   * Unregister the callbacks used to notify the framework of events and data
+   *
+   * Calling this function with an unregistered callback is a no-op
+   *
+   * @param callbacks callback to unregister
+   */
+  void unregisterAdapterStateCallbacks(in IUwbAdapterStateCallbacks callbacks);
+
+  /**
+   * Get the accuracy of the ranging timestamps
+   *
+   * @param chipId identifier of UWB chip for multi-HAL devices
+   *
+   * @return accuracy of the ranging timestamps in nanoseconds
+   */
+  long getTimestampResolutionNanos(in String chipId);
+
+  /**
+   * Provides the capabilities and features of the device
+   *
+   * @param chipId identifier of UWB chip for multi-HAL devices
+   *
+   * @return specification specific capabilities and features of the device
+   */
+  PersistableBundle getSpecificationInfo(in String chipId);
+
+  /**
+   * Request to open a new ranging session
+   *
+   * This function does not start the ranging session, but all necessary
+   * components must be initialized and ready to start a new ranging
+   * session prior to calling IUwbAdapterCallback#onRangingOpened.
+   *
+   * IUwbAdapterCallbacks#onRangingOpened must be called within
+   * RANGING_SESSION_OPEN_THRESHOLD_MS milliseconds of #openRanging being
+   * called if the ranging session is opened successfully.
+   *
+   * IUwbAdapterCallbacks#onRangingOpenFailed must be called within
+   * RANGING_SESSION_OPEN_THRESHOLD_MS milliseconds of #openRanging being called
+   * if the ranging session fails to be opened.
+   *
+   * If the provided sessionHandle is already open for the calling client, then
+   * #onRangingOpenFailed must be called and the new session must not be opened.
+   *
+   * @param attributionSource AttributionSource to use for permission enforcement.
+   * @param sessionHandle the session handle to open ranging for
+   * @param rangingCallbacks the callbacks used to deliver ranging information
+   * @param parameters the configuration to use for ranging
+   * @param chipId identifier of UWB chip for multi-HAL devices
+   */
+  void openRanging(in AttributionSource attributionSource,
+                   in SessionHandle sessionHandle,
+                   in IUwbRangingCallbacks rangingCallbacks,
+                   in PersistableBundle parameters,
+                   in String chipId);
+
+  /**
+   * Request to start ranging
+   *
+   * IUwbAdapterCallbacks#onRangingStarted must be called within
+   * RANGING_SESSION_START_THRESHOLD_MS milliseconds of #startRanging being
+   * called if the ranging session starts successfully.
+   *
+   * IUwbAdapterCallbacks#onRangingStartFailed must be called within
+   * RANGING_SESSION_START_THRESHOLD_MS milliseconds of #startRanging being
+   * called if the ranging session fails to be started.
+   *
+   * @param sessionHandle the session handle to start ranging for
+   * @param parameters additional configuration required to start ranging
+   */
+  void startRanging(in SessionHandle sessionHandle,
+                    in PersistableBundle parameters);
+
+  /**
+   * Request to reconfigure ranging
+   *
+   * IUwbAdapterCallbacks#onRangingReconfigured must be called after
+   * successfully reconfiguring the session.
+   *
+   * IUwbAdapterCallbacks#onRangingReconfigureFailed must be called after
+   * failing to reconfigure the session.
+   *
+   * A session must not be modified by a failed call to #reconfigureRanging.
+   *
+   * @param sessionHandle the session handle to start ranging for
+   * @param parameters the parameters to reconfigure and their new values
+   */
+  void reconfigureRanging(in SessionHandle sessionHandle,
+                          in PersistableBundle parameters);
+
+  /**
+   * Request to stop ranging
+   *
+   * IUwbAdapterCallbacks#onRangingStopped must be called after
+   * successfully stopping the session.
+   *
+   * IUwbAdapterCallbacks#onRangingStopFailed must be called after failing
+   * to stop the session.
+   *
+   * @param sessionHandle the session handle to stop ranging for
+   */
+  void stopRanging(in SessionHandle sessionHandle);
+
+  /**
+   * Close ranging for the session associated with the given handle
+   *
+   * Calling with an invalid handle or a handle that has already been closed
+   * is a no-op.
+   *
+   * IUwbAdapterCallbacks#onRangingClosed must be called within
+   * RANGING_SESSION_CLOSE_THRESHOLD_MS of #closeRanging being called.
+   *
+   * @param sessionHandle the session handle to close ranging for
+   */
+  void closeRanging(in SessionHandle sessionHandle);
+
+  /**
+   * Add a new controlee to an ongoing session.
+   * <p>This call may be made when the session is open.
+   *
+   * <p>On successfully adding a new controlee to the session
+   * {@link RangingSession.Callback#onControleeAdded(PersistableBundle)} is invoked.
+   *
+   * <p>On failure to add a new controlee to the session,
+   * {@link RangingSession.Callback#onControleeAddFailed(int, PersistableBundle)}is invoked.
+   *
+   * @param sessionHandle the session handle to close ranging for
+   * @param params the parameters for the new controlee.
+   */
+  void addControlee(in SessionHandle sessionHandle, in PersistableBundle params);
+
+  /**
+   * Remove an existing controlee from an ongoing session.
+   * <p>This call may be made when the session is open.
+   *
+   * <p>On successfully removing an existing controlee from the session
+   * {@link RangingSession.Callback#onControleeRemoved(PersistableBundle)} is invoked.
+   *
+   * <p>On failure to remove an existing controlee from the session,
+   * {@link RangingSession.Callback#onControleeRemoveFailed(int, PersistableBundle)}is invoked.
+   *
+   * @param sessionHandle the session handle to close ranging for
+   * @param params the parameters for the existing controlee.
+   */
+  void removeControlee(in SessionHandle sessionHandle, in PersistableBundle params);
+
+  /**
+   * Suspends an ongoing ranging session.
+   *
+   * <p>A session that has been pauseed may be resumed by calling
+   * {@link RangingSession#resume(PersistableBundle)} without the need to open a new session.
+   *
+   * <p>Suspending a {@link RangingSession} is useful when the lower layers should skip a few
+   * ranging rounds for a session without stopping it.
+   *
+   * <p>If the {@link RangingSession} is no longer needed, use {@link RangingSession#stop()} or
+   * {@link RangingSession#close()} to completely close the session.
+   *
+   * <p>On successfully pauseing the session,
+   * {@link RangingSession.Callback#onPaused(PersistableBundle)} is invoked.
+   *
+   * <p>On failure to pause the session,
+   * {@link RangingSession.Callback#onPauseFailed(int, PersistableBundle)} is invoked.
+   *
+   * @param sessionHandle the session handle to close ranging for
+   * @param params protocol specific parameters for pauseing the session.
+   */
+  void pause(in SessionHandle sessionHandle, in PersistableBundle params);
+
+  /**
+   * Resumes a pauseed ranging session.
+   *
+   * <p>A session that has been previously pauseed using
+   * {@link RangingSession#pause(PersistableBundle)} can be resumed by calling
+   * {@link RangingSession#resume(PersistableBundle)}.
+   *
+   * <p>On successfully resuming the session,
+   * {@link RangingSession.Callback#onResumed(PersistableBundle)} is invoked.
+   *
+   * <p>On failure to pause the session,
+   * {@link RangingSession.Callback#onResumeFailed(int, PersistableBundle)} is invoked.
+   *
+   * @param sessionHandle the session handle to close ranging for
+   * @param params protocol specific parameters the resuming the session.
+   */
+  void resume(in SessionHandle sessionHandle, in PersistableBundle params);
+
+  /**
+   * Send data to a remote device which is part of this ongoing session.
+   * The data is sent by piggybacking the provided data over RRM (initiator -> responder) or
+   * RIM (responder -> initiator).
+   * <p>This is only functional on a FIRA 2.0 compliant device.
+   *
+   * <p>On successfully sending the data,
+   * {@link RangingSession.Callback#onDataSent(UwbAddress, PersistableBundle)} is invoked.
+   *
+   * <p>On failure to send the data,
+   * {@link RangingSession.Callback#onDataSendFailed(UwbAddress, int, PersistableBundle)} is
+   * invoked.
+   *
+   * @param sessionHandle the session handle to close ranging for
+   * @param remoteDeviceAddress remote device's address.
+   * @param params protocol specific parameters the sending the data.
+   * @param data Raw data to be sent.
+   */
+  void sendData(in SessionHandle sessionHandle, in UwbAddress remoteDeviceAddress,
+          in PersistableBundle params, in byte[] data);
+
+  /**
+   * Disables or enables UWB for a user
+   *
+   * The provided callback's IUwbAdapterStateCallbacks#onAdapterStateChanged
+   * function must be called immediately following state change.
+   *
+   * @param enabled value representing intent to disable or enable UWB. If
+   * true, any subsequent calls to #openRanging will be allowed. If false,
+   * all active ranging sessions will be closed and subsequent calls to
+   * #openRanging will be disallowed.
+   */
+  void setEnabled(boolean enabled);
+
+  /**
+   * Returns the current enabled/disabled UWB state.
+   *
+   * Possible values are:
+   * IUwbAdapterState#STATE_DISABLED
+   * IUwbAdapterState#STATE_ENABLED_ACTIVE
+   * IUwbAdapterState#STATE_ENABLED_INACTIVE
+   *
+   * @return value representing enabled/disabled UWB state.
+   */
+  int getAdapterState();
+
+  /**
+   * Returns a list of UWB chip infos in a {@link PersistableBundle}.
+   *
+   * Callers can invoke methods on a specific UWB chip by passing its {@code chipId} to the
+   * method, which can be determined by calling:
+   * <pre>
+   * List<PersistableBundle> chipInfos = getChipInfos();
+   * for (PersistableBundle chipInfo : chipInfos) {
+   *     String chipId = ChipInfoParams.fromBundle(chipInfo).getChipId();
+   * }
+   * </pre>
+   *
+   * @return list of {@link PersistableBundle} containing info about UWB chips for a multi-HAL
+   * system, or a list of info for a single chip for a single HAL system.
+   */
+  List<PersistableBundle> getChipInfos();
+
+  List<String> getChipIds();
+
+  /**
+   * Returns the default UWB chip identifier.
+   *
+   * If callers do not pass a specific {@code chipId} to UWB methods, then the method will be
+   * invoked on the default chip, which is determined at system initialization from a configuration
+   * file.
+   *
+   * @return default UWB chip identifier for a multi-HAL system, or the identifier of the only UWB
+   * chip in a single HAL system.
+   */
+  String getDefaultChipId();
+
+  PersistableBundle addServiceProfile(in PersistableBundle parameters);
+
+  int removeServiceProfile(in PersistableBundle parameters);
+
+  PersistableBundle getAllServiceProfiles();
+
+  PersistableBundle getAdfProvisioningAuthorities(in PersistableBundle parameters);
+
+  PersistableBundle getAdfCertificateAndInfo(in PersistableBundle parameters);
+
+  void provisionProfileAdfByScript(in PersistableBundle serviceProfileBundle,
+            in IUwbAdfProvisionStateCallbacks callback);
+
+  int removeProfileAdf(in PersistableBundle serviceProfileBundle);
+
+  int sendVendorUciMessage(int gid, int oid, in byte[] payload);
+
+  /**
+   * The maximum allowed time to open a ranging session.
+   */
+  const int RANGING_SESSION_OPEN_THRESHOLD_MS = 3000; // Value TBD
+
+  /**
+   * The maximum allowed time to start a ranging session.
+   */
+  const int RANGING_SESSION_START_THRESHOLD_MS = 3000; // Value TBD
+
+  /**
+   * The maximum allowed time to notify the framework that a session has been
+   * closed.
+   */
+  const int RANGING_SESSION_CLOSE_THRESHOLD_MS = 3000; // Value TBD
+}
diff --git a/framework/java/android/uwb/IUwbAdapterStateCallbacks.aidl b/framework/java/android/uwb/IUwbAdapterStateCallbacks.aidl
new file mode 100644
index 0000000..05b77e2
--- /dev/null
+++ b/framework/java/android/uwb/IUwbAdapterStateCallbacks.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import android.uwb.StateChangeReason;
+import android.uwb.AdapterState;
+
+/**
+ * @hide
+ */
+oneway interface IUwbAdapterStateCallbacks {
+  /**
+     * Called whenever the adapter state changes
+     *
+     * @param state UWB state; enabled_active, enabled_inactive, or disabled.
+     * @param reason the reason that the state has changed
+     */
+    void onAdapterStateChanged(AdapterState state, StateChangeReason reason);
+}
\ No newline at end of file
diff --git a/framework/java/android/uwb/IUwbAdfProvisionStateCallbacks.aidl b/framework/java/android/uwb/IUwbAdfProvisionStateCallbacks.aidl
new file mode 100644
index 0000000..e5fc169
--- /dev/null
+++ b/framework/java/android/uwb/IUwbAdfProvisionStateCallbacks.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright 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.
+ */
+
+package android.uwb;
+
+import android.os.PersistableBundle;
+
+/**
+ * @hide
+ */
+oneway interface IUwbAdfProvisionStateCallbacks {
+    void onProfileAdfsProvisioned(in PersistableBundle params);
+    void onProfileAdfsProvisionFailed(int reason, in PersistableBundle params);
+}
diff --git a/framework/java/android/uwb/IUwbRangingCallbacks.aidl b/framework/java/android/uwb/IUwbRangingCallbacks.aidl
new file mode 100644
index 0000000..a961718
--- /dev/null
+++ b/framework/java/android/uwb/IUwbRangingCallbacks.aidl
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import android.os.PersistableBundle;
+import android.uwb.RangingChangeReason;
+import android.uwb.RangingReport;
+import android.uwb.SessionHandle;
+import android.uwb.UwbAddress;
+
+/**
+ * @hide
+ * TODO(b/211025367): Remove all the duplicate javadocs here.
+ */
+oneway interface IUwbRangingCallbacks {
+  /**
+   * Called when the ranging session has been opened
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   */
+  void onRangingOpened(in SessionHandle sessionHandle);
+
+  /**
+   * Called when a ranging session fails to start
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param reason the reason the session failed to start
+   * @param parameters protocol specific parameters
+   */
+  void onRangingOpenFailed(in SessionHandle sessionHandle,
+                           RangingChangeReason reason,
+                           in PersistableBundle parameters);
+
+  /**
+   * Called when ranging has started
+   *
+   * May output parameters generated by the lower layers that must be sent to the
+   * remote device(s). The PersistableBundle must be constructed using the UWB
+   * support library.
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param rangingOutputParameters parameters generated by the lower layer that
+   *                                should be sent to the remote device.
+   */
+  void onRangingStarted(in SessionHandle sessionHandle,
+                        in PersistableBundle parameters);
+
+  /**
+   * Called when a ranging session fails to start
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param reason the reason the session failed to start
+   * @param parameters protocol specific parameters
+   */
+  void onRangingStartFailed(in SessionHandle sessionHandle,
+                            RangingChangeReason reason,
+                            in PersistableBundle parameters);
+
+   /**
+   * Called when ranging has been reconfigured
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param parameters the updated ranging configuration
+   */
+  void onRangingReconfigured(in SessionHandle sessionHandle,
+                             in PersistableBundle parameters);
+
+  /**
+   * Called when a ranging session fails to be reconfigured
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param reason the reason the session failed to reconfigure
+   * @param parameters protocol specific parameters
+   */
+  void onRangingReconfigureFailed(in SessionHandle sessionHandle,
+                                  RangingChangeReason reason,
+                                  in PersistableBundle parameters);
+
+  /**
+   * Called when the ranging session has been stopped
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param reason the reason the session was stopped
+   * @param parameters protocol specific parameters
+   */
+
+  void onRangingStopped(in SessionHandle sessionHandle,
+                        RangingChangeReason reason,
+                        in PersistableBundle parameters);
+
+  /**
+   * Called when a ranging session fails to stop
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param reason the reason the session failed to stop
+   * @param parameters protocol specific parameters
+   */
+  void onRangingStopFailed(in SessionHandle sessionHandle,
+                           RangingChangeReason reason,
+                           in PersistableBundle parameters);
+
+  /**
+   * Called when a ranging session is closed
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param reason the reason the session was closed
+   * @param parameters protocol specific parameters
+   */
+  void onRangingClosed(in SessionHandle sessionHandle,
+                       RangingChangeReason reason,
+                       in PersistableBundle parameters);
+
+  /**
+   * Provides a new RangingResult to the framework
+   *
+   * The reported timestamp for a ranging measurement must be calculated as the
+   * time which the ranging round that generated this measurement concluded.
+   *
+   * @param sessionHandle an identifier to associate the ranging results with a
+   *                      session that is active
+   * @param result the ranging report
+   */
+  void onRangingResult(in SessionHandle sessionHandle, in RangingReport result);
+
+  /**
+   * Invoked when a new controlee is added to an ongoing one-to many session.
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param parameters protocol specific parameters for the new controlee.
+   */
+  void onControleeAdded(in SessionHandle sessionHandle, in PersistableBundle parameters);
+
+  /**
+   * Invoked when a new controlee is added to an ongoing one-to many session.
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param reason reason for the controlee add failure.
+   * @param parameters protocol specific parameters related to the failure.
+   */
+  void onControleeAddFailed(in SessionHandle sessionHandle,
+          RangingChangeReason reason, in PersistableBundle parameters);
+
+  /**
+   * Invoked when an existing controlee is removed from an ongoing one-to many session.
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param parameters protocol specific parameters for the existing controlee.
+   */
+  void onControleeRemoved(in SessionHandle sessionHandle, in PersistableBundle parameters);
+
+  /**
+   * Invoked when a new controlee is added to an ongoing one-to many session.
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param reason reason for the controlee remove failure.
+   * @param parameters protocol specific parameters related to the failure.
+   */
+  void onControleeRemoveFailed(in SessionHandle sessionHandle,
+          RangingChangeReason reason, in PersistableBundle parameters);
+
+  /**
+   * Invoked when an ongoing session is successfully suspended.
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param parameters protocol specific parameters sent for suspension.
+   */
+  void onRangingPaused(in SessionHandle sessionHandle, in PersistableBundle parameters);
+
+  /**
+   * Invoked when an ongoing session suspension fails.
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param reason reason for the suspension failure.
+   * @param parameters protocol specific parameters for suspension failure.
+   */
+  void onRangingPauseFailed(in SessionHandle sessionHandle,
+          RangingChangeReason reason, in PersistableBundle parameters);
+
+  /**
+   * Invoked when a suspended session is successfully resumed.
+   *
+   * @param parameters protocol specific parameters sent for suspension.
+   */
+  void onRangingResumed(in SessionHandle sessionHandle, in PersistableBundle parameters);
+
+  /**
+   * Invoked when a suspended session resumption fails.
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param reason reason for the resumption failure.
+   * @param parameters protocol specific parameters for resumption failure.
+   */
+  void onRangingResumeFailed(in SessionHandle sessionHandle,
+          RangingChangeReason reason, in PersistableBundle parameters);
+
+  /**
+   * Invoked when data is successfully sent via {@link RangingSession#sendData(UwbAddress,
+   * PersistableBundle, byte[])}.
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param remoteDeviceAddress remote device's address.
+   * @param parameters protocol specific parameters sent for suspension.
+   */
+  void onDataSent(in SessionHandle sessionHandle, in UwbAddress remoteDeviceAddress,
+          in PersistableBundle parameters);
+
+  /**
+   * Invoked when data send to a remote device via {@link RangingSession#sendData(UwbAddress,
+   * PersistableBundle, byte[])} fails.
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param remoteDeviceAddress remote device's address.
+   * @param reason reason for the resumption failure.
+   * @param parameters protocol specific parameters for resumption failure.
+   */
+  void onDataSendFailed(in SessionHandle sessionHandle, in UwbAddress remoteDeviceAddress,
+          RangingChangeReason reason, in PersistableBundle parameters);
+
+  /**
+   * Invoked when data is received successfully from a remote device.
+   * The data is received piggybacked over RRM (initiator -> responder) or
+   * RIM (responder -> initiator).
+   * <p> This is only functional on a FIRA 2.0 compliant device.
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param remoteDeviceAddress remote device's address.
+   * @param data Raw data received.
+   * @param parameters protocol specific parameters for the received data.
+   */
+  void onDataReceived(in SessionHandle sessionHandle, in UwbAddress remoteDeviceAddress,
+          in PersistableBundle parameters, in byte[] data);
+
+  /**
+   * Invoked when data receive from a remote device fails.
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param remoteDeviceAddress remote device's address.
+   * @param reason reason for the resumption failure.
+   * @param parameters protocol specific parameters for resumption failure.
+   */
+  void onDataReceiveFailed(in SessionHandle sessionHandle, in UwbAddress remoteDeviceAddress,
+          RangingChangeReason reason, in PersistableBundle parameters);
+
+  void onServiceDiscovered(in SessionHandle sessionHandle, in PersistableBundle parameters);
+
+  void onServiceConnected(in SessionHandle sessionHandle, in PersistableBundle parameters);
+}
diff --git a/framework/java/android/uwb/IUwbVendorUciCallback.aidl b/framework/java/android/uwb/IUwbVendorUciCallback.aidl
new file mode 100644
index 0000000..06ea8f3
--- /dev/null
+++ b/framework/java/android/uwb/IUwbVendorUciCallback.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright 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.
+ */
+package android.uwb;
+/**
+ * @hide
+ */
+oneway interface IUwbVendorUciCallback {
+    void onVendorResponseReceived(int gid, int oid, in byte[] payload);
+    void onVendorNotificationReceived(int gid, int oid, in byte[] payload);
+}
+
diff --git a/framework/java/android/uwb/MeasurementStatus.aidl b/framework/java/android/uwb/MeasurementStatus.aidl
new file mode 100644
index 0000000..5fa1554
--- /dev/null
+++ b/framework/java/android/uwb/MeasurementStatus.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+/**
+ * @hide
+ */
+@Backing(type="int")
+enum MeasurementStatus {
+  /**
+   * Ranging was successful
+   */
+  SUCCESS,
+
+  /**
+   * The remote device is out of range
+   */
+  FAILURE_OUT_OF_RANGE,
+
+  /**
+   * An unknown failure has occurred.
+   */
+   FAILURE_UNKNOWN,
+}
+
diff --git a/framework/java/android/uwb/RangingChangeReason.aidl b/framework/java/android/uwb/RangingChangeReason.aidl
new file mode 100644
index 0000000..deefb20
--- /dev/null
+++ b/framework/java/android/uwb/RangingChangeReason.aidl
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+/**
+ * @hide
+ * TODO(b/211025367): Remove this class and use direct API values.
+ */
+@Backing(type="int")
+enum RangingChangeReason {
+  /**
+   * Unknown reason
+   */
+  UNKNOWN,
+
+  /**
+   * A local API call triggered the change, such as a call to
+   * IUwbAdapter.closeRanging.
+   */
+  LOCAL_API,
+
+  /**
+   * The maximum number of sessions has been reached. This may be generated for
+   * an active session if a higher priority session begins.
+   */
+  MAX_SESSIONS_REACHED,
+
+  /**
+   * The system state has changed resulting in the session changing (e.g. the
+   * user disables UWB, or the user's locale changes and an active channel is no
+   * longer permitted to be used).
+   */
+  SYSTEM_POLICY,
+
+  /**
+   * The remote device has requested to change the session
+   */
+  REMOTE_REQUEST,
+
+  /**
+   * The session changed for a protocol specific reason
+   */
+  PROTOCOL_SPECIFIC,
+
+  /**
+   * The provided parameters were invalid
+   */
+  BAD_PARAMETERS,
+
+  /**
+   * Max ranging round retries reached.
+   */
+  MAX_RR_RETRY_REACHED,
+}
+
diff --git a/framework/java/android/uwb/RangingManager.java b/framework/java/android/uwb/RangingManager.java
new file mode 100644
index 0000000..df3b4c2
--- /dev/null
+++ b/framework/java/android/uwb/RangingManager.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.AttributionSource;
+import android.os.CancellationSignal;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.Hashtable;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ */
+public class RangingManager extends android.uwb.IUwbRangingCallbacks.Stub {
+    private static final String TAG = "Uwb.RangingManager";
+
+    private final IUwbAdapter mAdapter;
+    private final Hashtable<SessionHandle, RangingSession> mRangingSessionTable = new Hashtable<>();
+    private int mNextSessionId = 1;
+
+    public RangingManager(IUwbAdapter adapter) {
+        mAdapter = adapter;
+    }
+
+    /**
+     * Open a new ranging session
+     *
+     * @param attributionSource Attribution source to use for the enforcement of
+     *                          {@link android.Manifest.permission#ULTRAWIDEBAND_RANGING} runtime
+     *                          permission.
+     * @param params the parameters that define the ranging session
+     * @param executor {@link Executor} to run callbacks
+     * @param callbacks {@link RangingSession.Callback} to associate with the {@link RangingSession}
+     *                  that is being opened.
+     * @param chipId identifier of UWB chip to be used in ranging session, or {@code null} if
+     *                the default chip should be used
+     * @return a {@link CancellationSignal} that may be used to cancel the opening of the
+     *         {@link RangingSession}.
+     */
+    public CancellationSignal openSession(@NonNull AttributionSource attributionSource,
+            @NonNull PersistableBundle params,
+            @NonNull Executor executor,
+            @NonNull RangingSession.Callback callbacks,
+            @Nullable String chipId) {
+        if (chipId != null) {
+            try {
+                List<String> validChipIds = mAdapter.getChipIds();
+                if (!validChipIds.contains(chipId)) {
+                    throw new IllegalArgumentException("openSession - received invalid chipId: "
+                            + chipId);
+                }
+            } catch (RemoteException e)  {
+                e.rethrowFromSystemServer();
+            }
+        }
+
+        synchronized (this) {
+            SessionHandle sessionHandle = new SessionHandle(mNextSessionId++);
+            RangingSession session =
+                    new RangingSession(executor, callbacks, mAdapter, sessionHandle, chipId);
+            mRangingSessionTable.put(sessionHandle, session);
+            try {
+                mAdapter.openRanging(attributionSource,
+                        sessionHandle,
+                        this,
+                        params,
+                        chipId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+
+            CancellationSignal cancellationSignal = new CancellationSignal();
+            cancellationSignal.setOnCancelListener(() -> session.close());
+            return cancellationSignal;
+        }
+    }
+
+    private boolean hasSession(SessionHandle sessionHandle) {
+        return mRangingSessionTable.containsKey(sessionHandle);
+    }
+
+    @Override
+    public void onRangingOpened(SessionHandle sessionHandle) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG,
+                        "onRangingOpened - received unexpected SessionHandle: " + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingOpened();
+        }
+    }
+
+    @Override
+    public void onRangingOpenFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
+            PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG,
+                        "onRangingOpenedFailed - received unexpected SessionHandle: "
+                                + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingOpenFailed(convertToReason(reason), parameters);
+            mRangingSessionTable.remove(sessionHandle);
+        }
+    }
+
+    @Override
+    public void onRangingReconfigured(SessionHandle sessionHandle, PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG,
+                        "onRangingReconfigured - received unexpected SessionHandle: "
+                                + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingReconfigured(parameters);
+        }
+    }
+
+    @Override
+    public void onRangingReconfigureFailed(SessionHandle sessionHandle,
+            @RangingChangeReason int reason, PersistableBundle params) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onRangingReconfigureFailed - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingReconfigureFailed(convertToReason(reason), params);
+        }
+    }
+
+
+    @Override
+    public void onRangingStarted(SessionHandle sessionHandle, PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG,
+                        "onRangingStarted - received unexpected SessionHandle: " + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingStarted(parameters);
+        }
+    }
+
+    @Override
+    public void onRangingStartFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
+            PersistableBundle params) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onRangingStartFailed - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingStartFailed(convertToReason(reason), params);
+        }
+    }
+
+    @Override
+    public void onRangingStopped(SessionHandle sessionHandle, @RangingChangeReason int reason,
+            PersistableBundle params) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onRangingStopped - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingStopped(convertToReason(reason), params);
+        }
+    }
+
+    @Override
+    public void onRangingStopFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
+            PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onRangingStopFailed - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingStopFailed(convertToReason(reason), parameters);
+        }
+    }
+
+    @Override
+    public void onRangingClosed(SessionHandle sessionHandle, @RangingChangeReason int reason,
+            PersistableBundle params) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onRangingClosed - received unexpected SessionHandle: " + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingClosed(convertToReason(reason), params);
+            mRangingSessionTable.remove(sessionHandle);
+        }
+    }
+
+    @Override
+    public void onRangingResult(SessionHandle sessionHandle, RangingReport result) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onRangingResult - received unexpected SessionHandle: " + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingResult(result);
+        }
+    }
+
+    @Override
+    public void onControleeAdded(SessionHandle sessionHandle, PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onControleeAdded - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onControleeAdded(parameters);
+        }
+    }
+
+    @Override
+    public void onControleeAddFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
+            PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onControleeAddFailed - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onControleeAddFailed(reason, parameters);
+        }
+    }
+
+    @Override
+    public void onControleeRemoved(SessionHandle sessionHandle, PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onControleeRemoved - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onControleeRemoved(parameters);
+        }
+    }
+
+    @Override
+    public void onControleeRemoveFailed(SessionHandle sessionHandle,
+            @RangingChangeReason int reason, PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onControleeRemoveFailed - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onControleeRemoveFailed(reason, parameters);
+        }
+    }
+
+    @Override
+    public void onRangingPaused(SessionHandle sessionHandle, PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onRangingPaused - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingPaused(parameters);
+        }
+    }
+
+    @Override
+    public void onRangingPauseFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
+            PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onRangingPauseFailed - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingPauseFailed(reason, parameters);
+        }
+    }
+
+    @Override
+    public void onRangingResumed(SessionHandle sessionHandle, PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onRangingResumed - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingResumed(parameters);
+        }
+    }
+
+    @Override
+    public void onRangingResumeFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
+            PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onRangingResumeFailed - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingResumeFailed(reason, parameters);
+        }
+    }
+
+    @Override
+    public void onDataSent(SessionHandle sessionHandle, UwbAddress remoteDeviceAddress,
+            PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onDataSent - received unexpected SessionHandle: " + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onDataSent(remoteDeviceAddress, parameters);
+        }
+    }
+
+    @Override
+    public void onDataSendFailed(SessionHandle sessionHandle, UwbAddress remoteDeviceAddress,
+            @RangingChangeReason int reason, PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onDataSendFailed - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onDataSendFailed(remoteDeviceAddress, reason, parameters);
+        }
+    }
+
+    @Override
+    public void onDataReceived(SessionHandle sessionHandle, UwbAddress remoteDeviceAddress,
+            PersistableBundle parameters, byte[] data) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onDataReceived - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onDataReceived(remoteDeviceAddress, parameters, data);
+        }
+    }
+
+    @Override
+    public void onDataReceiveFailed(SessionHandle sessionHandle, UwbAddress remoteDeviceAddress,
+            @RangingChangeReason int reason, PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onDataReceiveFailed - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onDataReceiveFailed(remoteDeviceAddress, reason, parameters);
+        }
+    }
+
+    @Override
+    public void onServiceDiscovered(SessionHandle sessionHandle,
+            @NonNull PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onServiceDiscovered - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onServiceDiscovered(parameters);
+        }
+    }
+
+
+    @Override
+    public void onServiceConnected(SessionHandle sessionHandle,
+            @NonNull PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onServiceConnected - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onServiceConnected(parameters);
+        }
+    }
+
+    // TODO(b/211025367): Remove this conversion and use direct API values.
+    @RangingSession.Callback.Reason
+    private static int convertToReason(@RangingChangeReason int reason) {
+        switch (reason) {
+            case RangingChangeReason.LOCAL_API:
+                return RangingSession.Callback.REASON_LOCAL_REQUEST;
+
+            case RangingChangeReason.MAX_SESSIONS_REACHED:
+                return RangingSession.Callback.REASON_MAX_SESSIONS_REACHED;
+
+            case RangingChangeReason.SYSTEM_POLICY:
+                return RangingSession.Callback.REASON_SYSTEM_POLICY;
+
+            case RangingChangeReason.REMOTE_REQUEST:
+                return RangingSession.Callback.REASON_REMOTE_REQUEST;
+
+            case RangingChangeReason.PROTOCOL_SPECIFIC:
+                return RangingSession.Callback.REASON_PROTOCOL_SPECIFIC_ERROR;
+
+            case RangingChangeReason.BAD_PARAMETERS:
+                return RangingSession.Callback.REASON_BAD_PARAMETERS;
+
+            case RangingChangeReason.MAX_RR_RETRY_REACHED:
+                return RangingSession.Callback.REASON_MAX_RR_RETRY_REACHED;
+
+            case RangingChangeReason.UNKNOWN:
+            default:
+                return RangingSession.Callback.REASON_UNKNOWN;
+        }
+    }
+}
diff --git a/framework/java/android/uwb/RangingMeasurement.java b/framework/java/android/uwb/RangingMeasurement.java
new file mode 100644
index 0000000..d5f428d
--- /dev/null
+++ b/framework/java/android/uwb/RangingMeasurement.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Representation of a ranging measurement between the local device and a remote device
+ *
+ * @hide
+ */
+@SystemApi
+public final class RangingMeasurement implements Parcelable {
+    public static final int RSSI_UNKNOWN = -128;
+    public static final int RSSI_MIN = -127;
+    public static final int RSSI_MAX = -1;
+
+    private final UwbAddress mRemoteDeviceAddress;
+    private final @Status int mStatus;
+    private final long mElapsedRealtimeNanos;
+    private final DistanceMeasurement mDistanceMeasurement;
+    private final AngleOfArrivalMeasurement mAngleOfArrivalMeasurement;
+    private final AngleOfArrivalMeasurement mDestinationAngleOfArrivalMeasurement;
+    private final @LineOfSight int mLineOfSight;
+    private final @MeasurementFocus int mMeasurementFocus;
+    private final int mRssiDbm;
+
+    private RangingMeasurement(@NonNull UwbAddress remoteDeviceAddress, @Status int status,
+            long elapsedRealtimeNanos, @Nullable DistanceMeasurement distanceMeasurement,
+            @Nullable AngleOfArrivalMeasurement angleOfArrivalMeasurement,
+            @Nullable AngleOfArrivalMeasurement destinationAngleOfArrivalMeasurement,
+            @LineOfSight int lineOfSight, @MeasurementFocus int measurementFocus,
+            @IntRange(from = RSSI_UNKNOWN, to = RSSI_MAX) int rssiDbm) {
+        mRemoteDeviceAddress = remoteDeviceAddress;
+        mStatus = status;
+        mElapsedRealtimeNanos = elapsedRealtimeNanos;
+        mDistanceMeasurement = distanceMeasurement;
+        mAngleOfArrivalMeasurement = angleOfArrivalMeasurement;
+        mDestinationAngleOfArrivalMeasurement = destinationAngleOfArrivalMeasurement;
+        mLineOfSight = lineOfSight;
+        mMeasurementFocus = measurementFocus;
+        mRssiDbm = rssiDbm;
+    }
+
+    /**
+     * Get the remote device's {@link UwbAddress}
+     *
+     * @return the remote device's {@link UwbAddress}
+     */
+    @NonNull
+    public UwbAddress getRemoteDeviceAddress() {
+        return mRemoteDeviceAddress;
+    }
+
+    /**
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            RANGING_STATUS_SUCCESS,
+            RANGING_STATUS_FAILURE_OUT_OF_RANGE,
+            RANGING_STATUS_FAILURE_UNKNOWN_ERROR})
+    public @interface Status {}
+
+    /**
+     * Ranging attempt was successful for this device
+     */
+    public static final int RANGING_STATUS_SUCCESS = 0;
+
+    /**
+     * Ranging failed for this device because it is out of range
+     */
+    public static final int RANGING_STATUS_FAILURE_OUT_OF_RANGE = 1;
+
+    /**
+     * Ranging failed for this device because of unknown error
+     */
+    public static final int RANGING_STATUS_FAILURE_UNKNOWN_ERROR = -1;
+
+    /**
+     * Get the status of this ranging measurement
+     *
+     * <p>Possible values are
+     * {@link #RANGING_STATUS_SUCCESS},
+     * {@link #RANGING_STATUS_FAILURE_OUT_OF_RANGE},
+     * {@link #RANGING_STATUS_FAILURE_UNKNOWN_ERROR}.
+     *
+     * @return the status of the ranging measurement
+     */
+    @Status
+    public int getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * Timestamp of this ranging measurement in time since boot nanos in the same namespace as
+     * {@link SystemClock#elapsedRealtimeNanos()}
+     *
+     * @return timestamp of ranging measurement in nanoseconds
+     */
+    @SuppressLint("MethodNameUnits")
+    public long getElapsedRealtimeNanos() {
+        return mElapsedRealtimeNanos;
+    }
+
+    /**
+     * Get the distance measurement
+     *
+     * @return a {@link DistanceMeasurement} or null if {@link #getStatus()} !=
+     *         {@link #RANGING_STATUS_SUCCESS}
+     */
+    @Nullable
+    public DistanceMeasurement getDistanceMeasurement() {
+        return mDistanceMeasurement;
+    }
+
+    /**
+     * Get the angle of arrival measurement
+     *
+     * @return an {@link AngleOfArrivalMeasurement} or null if {@link #getStatus()} !=
+     *         {@link #RANGING_STATUS_SUCCESS}
+     */
+    @Nullable
+    public AngleOfArrivalMeasurement getAngleOfArrivalMeasurement() {
+        return mAngleOfArrivalMeasurement;
+    }
+
+    /**
+     * Get the angle of arrival measurement at the destination.
+     *
+     * @return an {@link AngleOfArrivalMeasurement} or null if {@link #getStatus()} !=
+     *         {@link #RANGING_STATUS_SUCCESS}
+     */
+    @Nullable
+    public AngleOfArrivalMeasurement getDestinationAngleOfArrivalMeasurement() {
+        return mDestinationAngleOfArrivalMeasurement;
+    }
+
+    /**
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            LOS,
+            NLOS,
+            LOS_UNDETERMINED})
+    public @interface LineOfSight {}
+
+    /**
+     * If measurement was in line of sight.
+     */
+    public static final int LOS = 0;
+
+    /**
+     * If measurement was not in line of sight.
+     */
+    public static final int NLOS = 1;
+
+    /**
+     * Unable to determine whether the measurement was in line of sight or not.
+     */
+    public static final int LOS_UNDETERMINED = 0xFF;
+
+    /**
+     * Get whether the measurement was in Line of sight or non-line of sight.
+     *
+     * @return whether the measurement was in line of sight or not
+     */
+    public @LineOfSight int getLineOfSight() {
+        return mLineOfSight;
+    }
+
+    /**
+     * Get the measured RSSI in dBm
+     */
+    public @IntRange(from = RSSI_UNKNOWN, to = RSSI_MAX) int getRssiDbm() {
+        return mRssiDbm;
+    }
+
+    /**
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            MEASUREMENT_FOCUS_NONE,
+            MEASUREMENT_FOCUS_RANGE,
+            MEASUREMENT_FOCUS_ANGLE_OF_ARRIVAL_AZIMUTH,
+            MEASUREMENT_FOCUS_ANGLE_OF_ARRIVAL_ELEVATION})
+    public @interface MeasurementFocus {}
+
+    /**
+     * Ranging measurement was done with no particular focus in terms of antenna selection.
+     */
+    public static final int MEASUREMENT_FOCUS_NONE = 0;
+
+    /**
+     * Ranging measurement was done with a focus on range calculation in terms of antenna
+     * selection.
+     */
+    public static final int MEASUREMENT_FOCUS_RANGE = 1;
+
+    /**
+     * Ranging measurement was done with a focus on Angle of arrival azimuth calculation in terms of
+     * antenna selection.
+     */
+    public static final int MEASUREMENT_FOCUS_ANGLE_OF_ARRIVAL_AZIMUTH = 2;
+
+    /**
+     * Ranging measurement was done with a focus on Angle of arrival elevation calculation in terms
+     * of antenna selection.
+     */
+    public static final int MEASUREMENT_FOCUS_ANGLE_OF_ARRIVAL_ELEVATION = 3;
+
+    /**
+     * Gets the measurement focus in terms of antenna used for this measurement.
+     *
+     * @return focus of this measurement.
+     */
+    public @MeasurementFocus int getMeasurementFocus() {
+        return mMeasurementFocus;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof RangingMeasurement) {
+            RangingMeasurement other = (RangingMeasurement) obj;
+            return Objects.equals(mRemoteDeviceAddress, other.getRemoteDeviceAddress())
+                    && mStatus == other.getStatus()
+                    && mElapsedRealtimeNanos == other.getElapsedRealtimeNanos()
+                    && Objects.equals(mDistanceMeasurement, other.getDistanceMeasurement())
+                    && Objects.equals(
+                            mAngleOfArrivalMeasurement, other.getAngleOfArrivalMeasurement())
+                    && Objects.equals(
+                            mDestinationAngleOfArrivalMeasurement,
+                            other.getDestinationAngleOfArrivalMeasurement())
+                    && mLineOfSight == other.getLineOfSight()
+                    && mMeasurementFocus == other.getMeasurementFocus()
+                    && mRssiDbm == other.getRssiDbm();
+        }
+        return false;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mRemoteDeviceAddress, mStatus, mElapsedRealtimeNanos,
+                mDistanceMeasurement, mAngleOfArrivalMeasurement,
+                mDestinationAngleOfArrivalMeasurement, mLineOfSight, mMeasurementFocus, mRssiDbm);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeParcelable(mRemoteDeviceAddress, flags);
+        dest.writeInt(mStatus);
+        dest.writeLong(mElapsedRealtimeNanos);
+        dest.writeParcelable(mDistanceMeasurement, flags);
+        dest.writeParcelable(mAngleOfArrivalMeasurement, flags);
+        dest.writeParcelable(mDestinationAngleOfArrivalMeasurement, flags);
+        dest.writeInt(mLineOfSight);
+        dest.writeInt(mMeasurementFocus);
+        dest.writeInt(mRssiDbm);
+    }
+
+    public static final @android.annotation.NonNull Creator<RangingMeasurement> CREATOR =
+            new Creator<RangingMeasurement>() {
+                @Override
+                public RangingMeasurement createFromParcel(Parcel in) {
+                    Builder builder = new Builder();
+                    builder.setRemoteDeviceAddress(
+                            in.readParcelable(UwbAddress.class.getClassLoader()));
+                    builder.setStatus(in.readInt());
+                    builder.setElapsedRealtimeNanos(in.readLong());
+                    builder.setDistanceMeasurement(
+                            in.readParcelable(DistanceMeasurement.class.getClassLoader()));
+                    builder.setAngleOfArrivalMeasurement(
+                            in.readParcelable(AngleOfArrivalMeasurement.class.getClassLoader()));
+                    builder.setDestinationAngleOfArrivalMeasurement(
+                            in.readParcelable(AngleOfArrivalMeasurement.class.getClassLoader()));
+                    builder.setLineOfSight(in.readInt());
+                    builder.setMeasurementFocus(in.readInt());
+                    builder.setRssiDbm(in.readInt());
+                    return builder.build();
+                }
+
+                @Override
+                public RangingMeasurement[] newArray(int size) {
+                    return new RangingMeasurement[size];
+                }
+    };
+
+    /** @hide **/
+    @Override
+    public String toString() {
+        return "RangingMeasurement["
+                + "distance measurement: " + mDistanceMeasurement
+                + ", aoa measurement: " + mAngleOfArrivalMeasurement
+                + ", dest aoa measurement: " + mDestinationAngleOfArrivalMeasurement
+                + ", lineOfSight: " + mLineOfSight
+                + ", rssiDbm: " + mRssiDbm
+                + "]";
+    }
+
+    /**
+     * Builder for a {@link RangingMeasurement} object.
+     */
+    public static final class Builder {
+        private UwbAddress mRemoteDeviceAddress = null;
+        private @Status int mStatus = RANGING_STATUS_FAILURE_UNKNOWN_ERROR;
+        private long mElapsedRealtimeNanos = -1L;
+        private DistanceMeasurement mDistanceMeasurement = null;
+        private AngleOfArrivalMeasurement mAngleOfArrivalMeasurement = null;
+        private AngleOfArrivalMeasurement mDestinationAngleOfArrivalMeasurement = null;
+        private @LineOfSight int mLineOfSight = LOS_UNDETERMINED;
+        private @MeasurementFocus int mMeasurementFocus = MEASUREMENT_FOCUS_NONE;
+        private int mRssiDbm = RSSI_UNKNOWN;
+
+        /**
+         * Set the remote device address that this measurement is for
+         *
+         * @param remoteDeviceAddress remote device's address
+         */
+        @NonNull
+        public Builder setRemoteDeviceAddress(@NonNull UwbAddress remoteDeviceAddress) {
+            mRemoteDeviceAddress = remoteDeviceAddress;
+            return this;
+        }
+
+        /**
+         * Set the status of ranging measurement
+         *
+         * @param status the status of the ranging measurement
+         */
+        @NonNull
+        public Builder setStatus(@Status int status) {
+            mStatus = status;
+            return this;
+        }
+
+        /**
+         * Set the elapsed realtime in nanoseconds when the ranging measurement occurred
+         *
+         * @param elapsedRealtimeNanos time the ranging measurement occurred
+         */
+        @NonNull
+        public Builder setElapsedRealtimeNanos(long elapsedRealtimeNanos) {
+            if (elapsedRealtimeNanos < 0) {
+                throw new IllegalArgumentException("elapsedRealtimeNanos must be >= 0");
+            }
+            mElapsedRealtimeNanos = elapsedRealtimeNanos;
+            return this;
+        }
+
+        /**
+         * Set the {@link DistanceMeasurement}
+         *
+         * @param distanceMeasurement the distance measurement for this ranging measurement
+         */
+        @NonNull
+        public Builder setDistanceMeasurement(@NonNull DistanceMeasurement distanceMeasurement) {
+            mDistanceMeasurement = distanceMeasurement;
+            return this;
+        }
+
+        /**
+         * Set the {@link AngleOfArrivalMeasurement}
+         *
+         * @param angleOfArrivalMeasurement the angle of arrival measurement for this ranging
+         *                                  measurement
+         */
+        @NonNull
+        public Builder setAngleOfArrivalMeasurement(
+                @NonNull AngleOfArrivalMeasurement angleOfArrivalMeasurement) {
+            mAngleOfArrivalMeasurement = angleOfArrivalMeasurement;
+            return this;
+        }
+
+        /**
+         * Set the {@link AngleOfArrivalMeasurement} at the destination.
+         *
+         * @param angleOfArrivalMeasurement the angle of arrival measurement for this ranging
+         *                                  measurement
+         */
+        @NonNull
+        public Builder setDestinationAngleOfArrivalMeasurement(
+                @NonNull AngleOfArrivalMeasurement angleOfArrivalMeasurement) {
+            mDestinationAngleOfArrivalMeasurement = angleOfArrivalMeasurement;
+            return this;
+        }
+
+        /**
+         * Set whether the measurement was in Line of sight or non-line of sight.
+         *
+         * @param lineOfSight whether the measurement was in line of sight or not
+         */
+        @NonNull
+        public Builder setLineOfSight(@LineOfSight int lineOfSight) {
+            mLineOfSight = lineOfSight;
+            return this;
+        }
+
+        /**
+         * Sets the measurement focus in terms of antenna used for this measurement.
+         *
+         * @param measurementFocus focus of this measurement.
+         */
+        @NonNull
+        public Builder setMeasurementFocus(@MeasurementFocus int measurementFocus) {
+            mMeasurementFocus = measurementFocus;
+            return this;
+        }
+
+        /**
+         * Set the RSSI in dBm
+         *
+         * @param rssiDbm the measured RSSI in dBm
+         */
+        @NonNull
+        public Builder setRssiDbm(@IntRange(from = RSSI_UNKNOWN, to = RSSI_MAX) int rssiDbm) {
+            if (rssiDbm != RSSI_UNKNOWN && (rssiDbm < RSSI_MIN || rssiDbm > RSSI_MAX)) {
+                throw new IllegalArgumentException("Invalid rssiDbm: " + rssiDbm);
+            }
+            mRssiDbm = rssiDbm;
+            return this;
+        }
+
+        /**
+         * Build the {@link RangingMeasurement} object
+         *
+         * @throws IllegalStateException if a distance or angle of arrival measurement is provided
+         *                               but the measurement was not successful, if the
+         *                               elapsedRealtimeNanos of the measurement is invalid, or
+         *                               if no remote device address is set
+         */
+        @NonNull
+        public RangingMeasurement build() {
+            if (mStatus != RANGING_STATUS_SUCCESS) {
+                if (mDistanceMeasurement != null) {
+                    throw new IllegalStateException(
+                            "Distance Measurement must be null if ranging is not successful");
+                }
+
+                if (mAngleOfArrivalMeasurement != null) {
+                    throw new IllegalStateException(
+                            "Angle of Arrival must be null if ranging is not successful");
+                }
+
+                // Destination AOA is optional according to the spec.
+            }
+
+            if (mRemoteDeviceAddress == null) {
+                throw new IllegalStateException("No remote device address was set");
+            }
+
+            if (mElapsedRealtimeNanos < 0) {
+                throw new IllegalStateException(
+                        "elapsedRealtimeNanos must be >=0: " + mElapsedRealtimeNanos);
+            }
+
+            return new RangingMeasurement(mRemoteDeviceAddress, mStatus, mElapsedRealtimeNanos,
+                    mDistanceMeasurement, mAngleOfArrivalMeasurement,
+                    mDestinationAngleOfArrivalMeasurement, mLineOfSight, mMeasurementFocus,
+                    mRssiDbm);
+        }
+    }
+}
diff --git a/framework/java/android/uwb/RangingReport.aidl b/framework/java/android/uwb/RangingReport.aidl
new file mode 100644
index 0000000..c32747a
--- /dev/null
+++ b/framework/java/android/uwb/RangingReport.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+parcelable RangingReport;
diff --git a/framework/java/android/uwb/RangingReport.java b/framework/java/android/uwb/RangingReport.java
new file mode 100644
index 0000000..75d2f30
--- /dev/null
+++ b/framework/java/android/uwb/RangingReport.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * This class contains the UWB ranging data
+ *
+ * @hide
+ */
+@SystemApi
+public final class RangingReport implements Parcelable {
+    private final List<RangingMeasurement> mRangingMeasurements;
+
+    private RangingReport(@NonNull List<RangingMeasurement> rangingMeasurements) {
+        mRangingMeasurements = rangingMeasurements;
+    }
+
+    /**
+     * Get a {@link List} of {@link RangingMeasurement} objects in the last measurement interval
+     * <p>The underlying UWB adapter may choose to do multiple measurements in each ranging
+     * interval.
+     *
+     * <p>The entries in the {@link List} are ordered in ascending order based on
+     * {@link RangingMeasurement#getElapsedRealtimeNanos()}
+     *
+     * @return a {@link List} of {@link RangingMeasurement} objects
+     */
+    @NonNull
+    public List<RangingMeasurement> getMeasurements() {
+        return mRangingMeasurements;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof RangingReport) {
+            RangingReport other = (RangingReport) obj;
+            return mRangingMeasurements.equals(other.getMeasurements());
+        }
+
+        return false;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mRangingMeasurements);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeTypedList(mRangingMeasurements);
+    }
+
+    public static final @android.annotation.NonNull Creator<RangingReport> CREATOR =
+            new Creator<RangingReport>() {
+                @Override
+                public RangingReport createFromParcel(Parcel in) {
+                    Builder builder = new Builder();
+                    builder.addMeasurements(in.createTypedArrayList(RangingMeasurement.CREATOR));
+                    return builder.build();
+                }
+
+                @Override
+                public RangingReport[] newArray(int size) {
+                    return new RangingReport[size];
+                }
+    };
+
+    /** @hide **/
+    @Override
+    public String toString() {
+        return "RangingReport["
+                + "measurements: " + mRangingMeasurements
+                + "]";
+    }
+
+    /**
+     * Builder for {@link RangingReport} object
+     */
+    public static final class Builder {
+        List<RangingMeasurement> mMeasurements = new ArrayList<>();
+
+        /**
+         * Add a single {@link RangingMeasurement}
+         *
+         * @param rangingMeasurement a ranging measurement
+         */
+        @NonNull
+        public Builder addMeasurement(@NonNull RangingMeasurement rangingMeasurement) {
+            mMeasurements.add(rangingMeasurement);
+            return this;
+        }
+
+        /**
+         * Add a {@link List} of {@link RangingMeasurement}s
+         *
+         * @param rangingMeasurements {@link List} of {@link RangingMeasurement}s to add
+         */
+        @NonNull
+        public Builder addMeasurements(@NonNull List<RangingMeasurement> rangingMeasurements) {
+            mMeasurements.addAll(rangingMeasurements);
+            return this;
+        }
+
+        /**
+         * Build the {@link RangingReport} object
+         *
+         * @throws IllegalStateException if measurements are not in monotonically increasing order
+         */
+        @NonNull
+        public RangingReport build() {
+            // Verify that all measurement timestamps are monotonically increasing
+            RangingMeasurement prevMeasurement = null;
+            for (int curIndex = 0; curIndex < mMeasurements.size(); curIndex++) {
+                RangingMeasurement curMeasurement = mMeasurements.get(curIndex);
+                if (prevMeasurement != null
+                        && (prevMeasurement.getElapsedRealtimeNanos()
+                                > curMeasurement.getElapsedRealtimeNanos())) {
+                    throw new IllegalStateException(
+                            "Timestamp (" + curMeasurement.getElapsedRealtimeNanos()
+                            + ") at index " + curIndex + " is less than previous timestamp ("
+                            + prevMeasurement.getElapsedRealtimeNanos() + ")");
+                }
+                prevMeasurement = curMeasurement;
+            }
+            return new RangingReport(mMeasurements);
+        }
+    }
+}
+
diff --git a/framework/java/android/uwb/RangingSession.java b/framework/java/android/uwb/RangingSession.java
new file mode 100644
index 0000000..7cf007c
--- /dev/null
+++ b/framework/java/android/uwb/RangingSession.java
@@ -0,0 +1,1034 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import android.Manifest;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.os.Binder;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+
+/**
+ * This class provides a way to control an active UWB ranging session.
+ * <p>It also defines the required {@link RangingSession.Callback} that must be implemented
+ * in order to be notified of UWB ranging results and status events related to the
+ * {@link RangingSession}.
+ *
+ * <p>To get an instance of {@link RangingSession}, first use
+ * {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)} to request to open a
+ * session. Once the session is opened, a {@link RangingSession} object is provided through
+ * {@link RangingSession.Callback#onOpened(RangingSession)}. If opening a session fails, the failure
+ * is reported through {@link RangingSession.Callback#onOpenFailed(int, PersistableBundle)} with the
+ * failure reason.
+ *
+ * @hide
+ */
+@SystemApi
+public final class RangingSession implements AutoCloseable {
+    private static final String TAG = "Uwb.RangingSession";
+    private final SessionHandle mSessionHandle;
+    private final IUwbAdapter mAdapter;
+    private final Executor mExecutor;
+    private final Callback mCallback;
+    private final String mChipId;
+
+    private enum State {
+        /**
+         * The state of the {@link RangingSession} until
+         * {@link RangingSession.Callback#onOpened(RangingSession)} is invoked
+         */
+        INIT,
+
+        /**
+         * The {@link RangingSession} is initialized and ready to begin ranging
+         */
+        IDLE,
+
+        /**
+         * The {@link RangingSession} is actively ranging
+         */
+        ACTIVE,
+
+        /**
+         * The {@link RangingSession} is closed and may not be used for ranging.
+         */
+        CLOSED
+    }
+
+    private State mState = State.INIT;
+
+    /**
+     * Interface for receiving {@link RangingSession} events
+     */
+    public interface Callback {
+        /**
+         * @hide
+         */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(value = {
+                REASON_UNKNOWN,
+                REASON_LOCAL_REQUEST,
+                REASON_REMOTE_REQUEST,
+                REASON_BAD_PARAMETERS,
+                REASON_GENERIC_ERROR,
+                REASON_MAX_SESSIONS_REACHED,
+                REASON_SYSTEM_POLICY,
+                REASON_PROTOCOL_SPECIFIC_ERROR,
+                REASON_MAX_RR_RETRY_REACHED,
+                REASON_SERVICE_DISCOVERY_FAILURE,
+                REASON_SERVICE_CONNECTION_FAILURE,
+                REASON_SE_NOT_SUPPORTED,
+                REASON_SE_INTERACTION_FAILURE,
+        })
+        @interface Reason {}
+
+        /**
+         * Indicates that the session was closed or failed to open due to an unknown reason
+         */
+        int REASON_UNKNOWN = 0;
+
+        /**
+         * Indicates that the session was closed or failed to open because
+         * {@link AutoCloseable#close()} or {@link RangingSession#close()} was called
+         */
+        int REASON_LOCAL_REQUEST = 1;
+
+        /**
+         * Indicates that the session was closed or failed to open due to an explicit request from
+         * the remote device.
+         */
+        int REASON_REMOTE_REQUEST = 2;
+
+        /**
+         * Indicates that the session was closed or failed to open due to erroneous parameters
+         */
+        int REASON_BAD_PARAMETERS = 3;
+
+        /**
+         * Indicates an error on this device besides the error code already listed
+         */
+        int REASON_GENERIC_ERROR = 4;
+
+        /**
+         * Indicates that the number of currently open sessions supported by the device and
+         * additional sessions may not be opened.
+         */
+        int REASON_MAX_SESSIONS_REACHED = 5;
+
+        /**
+         * Indicates that the local system policy caused the change, such
+         * as privacy policy, power management policy, permissions, and more.
+         */
+        int REASON_SYSTEM_POLICY = 6;
+
+        /**
+         * Indicates a protocol specific error. The associated {@link PersistableBundle} should be
+         * consulted for additional information.
+         */
+        int REASON_PROTOCOL_SPECIFIC_ERROR = 7;
+
+        /**
+         * Indicates that the max number of retry attempts for a ranging attempt has been reached.
+         */
+        int REASON_MAX_RR_RETRY_REACHED = 9;
+
+        /**
+         * Indicates a failure to discover the service after activation.
+         */
+        int REASON_SERVICE_DISCOVERY_FAILURE = 10;
+
+        /**
+         * Indicates a failure to connect to the service after discovery.
+         */
+        int REASON_SERVICE_CONNECTION_FAILURE = 11;
+
+        /**
+         * The device doesn’t support FiRA Applet.
+         */
+        int REASON_SE_NOT_SUPPORTED = 12;
+
+        /**
+         * SE interactions failed.
+         */
+        int REASON_SE_INTERACTION_FAILURE = 13;
+
+        /**
+         * @hide
+         */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(value = {
+                CONTROLEE_FAILURE_REASON_MAX_CONTROLEE_REACHED,
+        })
+        @interface ControleeFailureReason {}
+
+        /**
+         * Indicates that the session has reached the max number of controlees supported by the
+         * device. This is applicable to only one to many sessions and sent in response to a
+         * request to add a new controlee to an ongoing session.
+         */
+        int CONTROLEE_FAILURE_REASON_MAX_CONTROLEE_REACHED = 0;
+
+        /**
+         * @hide
+         */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(value = {
+                DATA_FAILURE_REASON_DATA_SIZE_TOO_LARGE,
+        })
+        @interface DataFailureReason {}
+
+        /**
+         * Indicates that the size of the data being sent or received is too large.
+         */
+        int DATA_FAILURE_REASON_DATA_SIZE_TOO_LARGE = 10;
+
+        /**
+         * Invoked when {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)}
+         * is successful
+         *
+         * @param session the newly opened {@link RangingSession}
+         */
+        void onOpened(@NonNull RangingSession session);
+
+        /**
+         * Invoked if {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)}}
+         * fails
+         *
+         * @param reason the failure reason
+         * @param params protocol specific parameters
+         */
+        void onOpenFailed(@Reason int reason, @NonNull PersistableBundle params);
+
+        /**
+         * Invoked either,
+         *  - when {@link RangingSession#start(PersistableBundle)} is successful if the session is
+         *    using a custom profile, OR
+         *  - when platform starts ranging after OOB discovery + negotiation if the session is
+         *    using a platform defined profile.
+         * @param sessionInfo session specific parameters from the lower layers
+         */
+        void onStarted(@NonNull PersistableBundle sessionInfo);
+
+        /**
+         * Invoked either,
+         *   - when {@link RangingSession#start(PersistableBundle)} fails if
+         *     the session is using a custom profile, OR
+         *   - when platform fails ranging after OOB discovery + negotiation if the
+         *     session is using a platform defined profile.
+         *
+         * @param reason the failure reason
+         * @param params protocol specific parameters
+         */
+        void onStartFailed(@Reason int reason, @NonNull PersistableBundle params);
+
+        /**
+         * Invoked when a request to reconfigure the session succeeds
+         *
+         * @param params the updated ranging configuration
+         */
+        void onReconfigured(@NonNull PersistableBundle params);
+
+        /**
+         * Invoked when a request to reconfigure the session fails
+         *
+         * @param reason reason the session failed to be reconfigured
+         * @param params protocol specific failure reasons
+         */
+        void onReconfigureFailed(@Reason int reason, @NonNull PersistableBundle params);
+
+        /**
+         * Invoked when a request to stop the session succeeds
+         *
+         * @param reason reason for the session stop
+         * @param parameters protocol specific parameters related to the stop reason
+         */
+        void onStopped(@Reason int reason, @NonNull PersistableBundle parameters);
+
+        /**
+         * Invoked when a request to stop the session fails
+         *
+         * @param reason reason the session failed to be stopped
+         * @param params protocol specific failure reasons
+         */
+        void onStopFailed(@Reason int reason, @NonNull PersistableBundle params);
+
+       /**
+         * Invoked when session is either closed spontaneously, or per user request via
+         * {@link RangingSession#close()} or {@link AutoCloseable#close()}.
+         *
+         * @param reason reason for the session closure
+         * @param parameters protocol specific parameters related to the close reason
+         */
+        void onClosed(@Reason int reason, @NonNull PersistableBundle parameters);
+
+        /**
+         * Called once per ranging interval even when a ranging measurement fails
+         *
+         * @param rangingReport ranging report for this interval's measurements
+         */
+        void onReportReceived(@NonNull RangingReport rangingReport);
+
+        /**
+         * Invoked when a new controlee is added to an ongoing one-to many session.
+         *
+         * @param parameters protocol specific parameters for the new controlee
+         */
+        default void onControleeAdded(@NonNull PersistableBundle parameters) {}
+
+        /**
+         * Invoked when a new controlee is added to an ongoing one-to many session.
+         *
+         * @param reason reason for the controlee add failure
+         * @param parameters protocol specific parameters related to the failure
+         */
+        default void onControleeAddFailed(
+                @ControleeFailureReason int reason, @NonNull PersistableBundle parameters) {}
+
+        /**
+         * Invoked when an existing controlee is removed from an ongoing one-to many session.
+         *
+         * @param parameters protocol specific parameters for the existing controlee
+         */
+        default void onControleeRemoved(@NonNull PersistableBundle parameters) {}
+
+        /**
+         * Invoked when a new controlee is added to an ongoing one-to many session.
+         *
+         * @param reason reason for the controlee remove failure
+         * @param parameters protocol specific parameters related to the failure
+         */
+        default void onControleeRemoveFailed(
+                @ControleeFailureReason int reason, @NonNull PersistableBundle parameters) {}
+
+        /**
+         * Invoked when an ongoing session is successfully pauseed.
+         *
+         * @param parameters protocol specific parameters sent for suspension
+         */
+        default void onPaused(@NonNull PersistableBundle parameters) {}
+
+        /**
+         * Invoked when an ongoing session suspension fails.
+         *
+         * @param reason reason for the suspension failure
+         * @param parameters protocol specific parameters for suspension failure
+         */
+        default void onPauseFailed(@Reason int reason, @NonNull PersistableBundle parameters) {}
+
+        /**
+         * Invoked when a pauseed session is successfully resumed.
+         *
+         * @param parameters protocol specific parameters sent for suspension
+         */
+        default void onResumed(@NonNull PersistableBundle parameters) {}
+
+        /**
+         * Invoked when a pauseed session resumption fails.
+         *
+         * @param reason reason for the resumption failure
+         * @param parameters protocol specific parameters for resumption failure
+         */
+        default void onResumeFailed(@Reason int reason, @NonNull PersistableBundle parameters) {}
+
+        /**
+         * Invoked when data is successfully sent via {@link RangingSession#sendData(UwbAddress,
+         * PersistableBundle, byte[])}.
+         *
+         * @param remoteDeviceAddress remote device's address
+         * @param parameters protocol specific parameters sent for suspension
+         */
+        default void onDataSent(@NonNull UwbAddress remoteDeviceAddress,
+                @NonNull PersistableBundle parameters) {}
+
+        /**
+         * Invoked when data send to a remote device via {@link RangingSession#sendData(UwbAddress,
+         * PersistableBundle, byte[])} fails.
+         *
+         * @param remoteDeviceAddress remote device's address
+         * @param reason reason for the resumption failure
+         * @param parameters protocol specific parameters for resumption failure
+         */
+        default void onDataSendFailed(@NonNull UwbAddress remoteDeviceAddress,
+                @DataFailureReason int reason, @NonNull PersistableBundle parameters) {}
+
+        /**
+         * Invoked when data is received successfully from a remote device.
+         * The data is received piggybacked over RRM (initiator -> responder) or
+         * RIM (responder -> initiator).
+         * <p> This is only functional on a FIRA 2.0 compliant device.
+         *
+         * @param remoteDeviceAddress remote device's address
+         * @param data Raw data received
+         * @param parameters protocol specific parameters for the received data
+         */
+        default void onDataReceived(@NonNull UwbAddress remoteDeviceAddress,
+                @NonNull PersistableBundle parameters, @NonNull byte[] data) {}
+
+        /**
+         * Invoked when data receive from a remote device fails.
+         *
+         * @param remoteDeviceAddress remote device's address
+         * @param reason reason for the reception failure
+         * @param parameters protocol specific parameters for resumption failure
+         */
+        default void onDataReceiveFailed(@NonNull UwbAddress remoteDeviceAddress,
+                @DataFailureReason int reason, @NonNull PersistableBundle parameters) {}
+
+        /**
+         * Invoked when service is discovered via OOB.
+         * <p>
+         * If this a one to many session, this can be invoked multiple times to indicate different
+         * peers being discovered.
+         * </p>
+         *
+         * @param parameters protocol specific params for discovered service.
+         */
+        default void onServiceDiscovered(@NonNull PersistableBundle parameters) {}
+
+        /**
+         * Invoked when service is connected via OOB.
+         * <p>
+         * If this a one to many session, this can be invoked multiple times to indicate different
+         * peers being connected.
+         * </p>
+         *
+         * @param parameters protocol specific params for connected service.
+         */
+        default void onServiceConnected(@NonNull PersistableBundle parameters) {}
+    }
+
+    /**
+     * @hide
+     */
+    public RangingSession(Executor executor, Callback callback, IUwbAdapter adapter,
+            SessionHandle sessionHandle) {
+        this(executor, callback, adapter, sessionHandle, /* chipId= */ null);
+    }
+
+    /**
+     * @hide
+     */
+    public RangingSession(Executor executor, Callback callback, IUwbAdapter adapter,
+            SessionHandle sessionHandle, String chipId) {
+        mState = State.INIT;
+        mExecutor = executor;
+        mCallback = callback;
+        mAdapter = adapter;
+        mSessionHandle = sessionHandle;
+        mChipId = chipId;
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isOpen() {
+        return mState == State.IDLE || mState == State.ACTIVE;
+    }
+
+    /**
+     * If the session uses custom profile,
+     *    Begins ranging for the session.
+     *    <p>On successfully starting a ranging session,
+     *    {@link RangingSession.Callback#onStarted(PersistableBundle)} is invoked.
+     *    <p>On failure to start the session,
+     *    {@link RangingSession.Callback#onStartFailed(int, PersistableBundle)}
+     *    is invoked.
+     *
+     * If the session uses platform defined profile (like PACS),
+     *    Begins OOB discovery for the service. Once the service is discovered,
+     *    UWB session params are negotiated via OOB and a UWB session will be
+     *    started.
+     *    <p>On successfully discovering a service,
+     *    {@link RangingSession.Callback#onServiceDiscovered(PersistableBundle)} is invoked.
+     *    <p>On successfully connecting to a service,
+     *    {@link RangingSession.Callback#onServiceConnected(PersistableBundle)} is invoked.
+     *    <p>On successfully starting a ranging session,
+     *    {@link RangingSession.Callback#onStarted(PersistableBundle)} is invoked.
+     *    <p>On failure to start the session,
+     *    {@link RangingSession.Callback#onStartFailed(int, PersistableBundle)}
+     *    is invoked.
+     *
+     * @param params configuration parameters for starting the session
+     */
+    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
+    public void start(@NonNull PersistableBundle params) {
+        if (mState != State.IDLE) {
+            throw new IllegalStateException();
+        }
+
+        try {
+            mAdapter.startRanging(mSessionHandle, params);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Attempts to reconfigure the session with the given parameters
+     * <p>This call may be made when the session is open.
+     *
+     * <p>On successfully reconfiguring the session
+     * {@link RangingSession.Callback#onReconfigured(PersistableBundle)} is invoked.
+     *
+     * <p>On failure to reconfigure the session,
+     * {@link RangingSession.Callback#onReconfigureFailed(int, PersistableBundle)} is invoked.
+     *
+     * @param params the parameters to reconfigure and their new values
+     */
+    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
+    public void reconfigure(@NonNull PersistableBundle params) {
+        if (mState != State.ACTIVE && mState != State.IDLE) {
+            throw new IllegalStateException();
+        }
+
+        try {
+            mAdapter.reconfigureRanging(mSessionHandle, params);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Stops actively ranging
+     *
+     * <p>A session that has been stopped may be resumed by calling
+     * {@link RangingSession#start(PersistableBundle)} without the need to open a new session.
+     *
+     * <p>Stopping a {@link RangingSession} is useful when the lower layers should not discard
+     * the parameters of the session, or when a session needs to be able to be resumed quickly.
+     *
+     * <p>If the {@link RangingSession} is no longer needed, use {@link RangingSession#close()} to
+     * completely close the session and allow lower layers of the stack to perform necessarily
+     * cleanup.
+     *
+     * <p>Stopped sessions may be closed by the system at any time. In such a case,
+     * {@link RangingSession.Callback#onClosed(int, PersistableBundle)} is invoked.
+     *
+     * <p>On failure to stop the session,
+     * {@link RangingSession.Callback#onStopFailed(int, PersistableBundle)} is invoked.
+     */
+    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
+    public void stop() {
+        if (mState != State.ACTIVE) {
+            throw new IllegalStateException();
+        }
+
+        try {
+            mAdapter.stopRanging(mSessionHandle);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Close the ranging session
+     *
+     * <p>After calling this function, in order resume ranging, a new {@link RangingSession} must
+     * be opened by calling
+     * {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)}.
+     *
+     * <p>If this session is currently ranging, it will stop and close the session.
+     * <p>If the session is in the process of being opened, it will attempt to stop the session from
+     * being opened.
+     * <p>If the session is already closed, the registered
+     * {@link Callback#onClosed(int, PersistableBundle)} callback will still be invoked.
+     *
+     * <p>{@link Callback#onClosed(int, PersistableBundle)} will be invoked using the same callback
+     * object given to {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)}
+     * when the {@link RangingSession} was opened. The callback will be invoked after each call to
+     * {@link #close()}, even if the {@link RangingSession} is already closed.
+     */
+    @Override
+    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
+    public void close() {
+        if (mState == State.CLOSED) {
+            mExecutor.execute(() -> mCallback.onClosed(
+                    Callback.REASON_LOCAL_REQUEST, new PersistableBundle()));
+            return;
+        }
+
+        try {
+            mAdapter.closeRanging(mSessionHandle);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Add a new controlee to an ongoing session.
+     * <p>This call may be made when the session is open.
+     *
+     * <p>On successfully adding a new controlee to the session
+     * {@link RangingSession.Callback#onControleeAdded(PersistableBundle)} is invoked.
+     *
+     * <p>On failure to add a new controlee to the session,
+     * {@link RangingSession.Callback#onControleeAddFailed(int, PersistableBundle)} is invoked.
+     *
+     * @param params the parameters for the new controlee
+     */
+    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
+    public void addControlee(@NonNull PersistableBundle params) {
+        if (mState != State.ACTIVE && mState != State.IDLE) {
+            throw new IllegalStateException();
+        }
+
+        try {
+            mAdapter.addControlee(mSessionHandle, params);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove an existing controlee from an ongoing session.
+     * <p>This call may be made when the session is open.
+     *
+     * <p>On successfully removing an existing controlee from the session
+     * {@link RangingSession.Callback#onControleeRemoved(PersistableBundle)} is invoked.
+     *
+     * <p>On failure to remove an existing controlee from the session,
+     * {@link RangingSession.Callback#onControleeRemoveFailed(int, PersistableBundle)} is invoked.
+     *
+     * @param params the parameters for the existing controlee
+     */
+    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
+    public void removeControlee(@NonNull PersistableBundle params) {
+        if (mState != State.ACTIVE && mState != State.IDLE) {
+            throw new IllegalStateException();
+        }
+
+        try {
+            mAdapter.removeControlee(mSessionHandle, params);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Pauses an ongoing ranging session.
+     *
+     * <p>A session that has been pauseed may be resumed by calling
+     * {@link RangingSession#resume(PersistableBundle)} without the need to open a new session.
+     *
+     * <p>Pauseing a {@link RangingSession} is useful when the lower layers should skip a few
+     * ranging rounds for a session without stopping it.
+     *
+     * <p>If the {@link RangingSession} is no longer needed, use {@link RangingSession#stop()} or
+     * {@link RangingSession#close()} to completely close the session.
+     *
+     * <p>On successfully pausing the session,
+     * {@link RangingSession.Callback#onRangingPaused(PersistableBundle)} is invoked.
+     *
+     * <p>On failure to pause the session,
+     * {@link RangingSession.Callback#onRangingPauseFailed(int, PersistableBundle)} is invoked.
+     *
+     * @param params protocol specific parameters for pausing the session
+     */
+    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
+    public void pause(@NonNull PersistableBundle params) {
+        if (mState != State.ACTIVE) {
+            throw new IllegalStateException();
+        }
+
+        try {
+            mAdapter.pause(mSessionHandle, params);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Resumes a pauseed ranging session.
+     *
+     * <p>A session that has been previously pauseed using
+     * {@link RangingSession#pause(PersistableBundle)} can be resumed by calling
+     * {@link RangingSession#resume(PersistableBundle)}.
+     *
+     * <p>On successfully resuming the session,
+     * {@link RangingSession.Callback#onRangingResumed(PersistableBundle)} is invoked.
+     *
+     * <p>On failure to resume the session,
+     * {@link RangingSession.Callback#onRangingResumeFailed(int, PersistableBundle)} is invoked.
+     *
+     * @param params protocol specific parameters the resuming the session
+     */
+    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
+    public void resume(@NonNull PersistableBundle params) {
+        if (mState != State.ACTIVE) {
+            throw new IllegalStateException();
+        }
+
+        try {
+            mAdapter.resume(mSessionHandle, params);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Send data to a remote device which is part of this ongoing session.
+     * The data is sent by piggybacking the provided data over RRM (initiator -> responder) or
+     * RIM (responder -> initiator).
+     * <p>This is only functional on a FIRA 2.0 compliant device.
+     *
+     * <p>On successfully sending the data,
+     * {@link RangingSession.Callback#onDataSent(UwbAddress, PersistableBundle)} is invoked.
+     *
+     * <p>On failure to send the data,
+     * {@link RangingSession.Callback#onDataSendFailed(UwbAddress, int, PersistableBundle)} is
+     * invoked.
+     *
+     * @param remoteDeviceAddress remote device's address
+     * @param params protocol specific parameters the sending the data
+     * @param data Raw data to be sent
+     */
+    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
+    public void sendData(@NonNull UwbAddress remoteDeviceAddress,
+            @NonNull PersistableBundle params, @NonNull byte[] data) {
+        if (mState != State.ACTIVE) {
+            throw new IllegalStateException();
+        }
+
+        try {
+            mAdapter.sendData(mSessionHandle, remoteDeviceAddress, params, data);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingOpened() {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingOpened invoked for a closed session");
+            return;
+        }
+
+        mState = State.IDLE;
+        executeCallback(() -> mCallback.onOpened(this));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingOpenFailed(@Callback.Reason int reason,
+            @NonNull PersistableBundle params) {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingOpenFailed invoked for a closed session");
+            return;
+        }
+
+        mState = State.CLOSED;
+        executeCallback(() -> mCallback.onOpenFailed(reason, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingStarted(@NonNull PersistableBundle parameters) {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingStarted invoked for a closed session");
+            return;
+        }
+
+        mState = State.ACTIVE;
+        executeCallback(() -> mCallback.onStarted(parameters));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingStartFailed(@Callback.Reason int reason,
+            @NonNull PersistableBundle params) {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingStartFailed invoked for a closed session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onStartFailed(reason, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingReconfigured(@NonNull PersistableBundle params) {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingReconfigured invoked for a closed session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onReconfigured(params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingReconfigureFailed(@Callback.Reason int reason,
+            @NonNull PersistableBundle params) {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingReconfigureFailed invoked for a closed session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onReconfigureFailed(reason, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingStopped(@Callback.Reason int reason,
+            @NonNull PersistableBundle params) {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingStopped invoked for a closed session");
+            return;
+        }
+
+        mState = State.IDLE;
+        executeCallback(() -> mCallback.onStopped(reason, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingStopFailed(@Callback.Reason int reason,
+            @NonNull PersistableBundle params) {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingStopFailed invoked for a closed session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onStopFailed(reason, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingClosed(@Callback.Reason int reason,
+            @NonNull PersistableBundle parameters) {
+        mState = State.CLOSED;
+        executeCallback(() -> mCallback.onClosed(reason, parameters));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingResult(@NonNull RangingReport report) {
+        if (!isOpen()) {
+            Log.w(TAG, "onRangingResult invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onReportReceived(report));
+    }
+
+    /**
+     * @hide
+     */
+    public void onControleeAdded(@NonNull PersistableBundle params) {
+        if (!isOpen()) {
+            Log.w(TAG, "onControleeAdded invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onControleeAdded(params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onControleeAddFailed(@Callback.ControleeFailureReason int reason,
+            @NonNull PersistableBundle params) {
+        if (!isOpen()) {
+            Log.w(TAG, "onControleeAddFailed invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onControleeAddFailed(reason, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onControleeRemoved(@NonNull PersistableBundle params) {
+        if (!isOpen()) {
+            Log.w(TAG, "onControleeRemoved invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onControleeRemoved(params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onControleeRemoveFailed(@Callback.ControleeFailureReason int reason,
+            @NonNull PersistableBundle params) {
+        if (!isOpen()) {
+            Log.w(TAG, "onControleeRemoveFailed invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onControleeRemoveFailed(reason, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingPaused(@NonNull PersistableBundle params) {
+        if (!isOpen()) {
+            Log.w(TAG, "onRangingPaused invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onPaused(params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingPauseFailed(@Callback.Reason int reason,
+            @NonNull PersistableBundle params) {
+        if (!isOpen()) {
+            Log.w(TAG, "onRangingPauseFailed invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onPauseFailed(reason, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingResumed(@NonNull PersistableBundle params) {
+        if (!isOpen()) {
+            Log.w(TAG, "onRangingResumed invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onResumed(params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingResumeFailed(@Callback.Reason int reason,
+            @NonNull PersistableBundle params) {
+        if (!isOpen()) {
+            Log.w(TAG, "onRangingResumeFailed invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onResumeFailed(reason, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onDataSent(@NonNull UwbAddress remoteDeviceAddress,
+            @NonNull PersistableBundle params) {
+        if (!isOpen()) {
+            Log.w(TAG, "onDataSent invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onDataSent(remoteDeviceAddress, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onDataSendFailed(@NonNull UwbAddress remoteDeviceAddress,
+            @Callback.DataFailureReason int reason, @NonNull PersistableBundle params) {
+        if (!isOpen()) {
+            Log.w(TAG, "onDataSendFailed invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onDataSendFailed(remoteDeviceAddress, reason, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onDataReceived(@NonNull UwbAddress remoteDeviceAddress,
+            @NonNull PersistableBundle params, @NonNull byte[] data) {
+        if (!isOpen()) {
+            Log.w(TAG, "onDataReceived invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onDataReceived(remoteDeviceAddress, params, data));
+    }
+
+    /**
+     * @hide
+     */
+    public void onDataReceiveFailed(@NonNull UwbAddress remoteDeviceAddress,
+            @Callback.DataFailureReason int reason, @NonNull PersistableBundle params) {
+        if (!isOpen()) {
+            Log.w(TAG, "onDataReceiveFailed invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onDataReceiveFailed(remoteDeviceAddress, reason, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onServiceDiscovered(@NonNull PersistableBundle params) {
+        if (!isOpen()) {
+            Log.w(TAG, "onServiceDiscovered invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onServiceDiscovered(params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onServiceConnected(@NonNull PersistableBundle params) {
+        if (!isOpen()) {
+            Log.w(TAG, "onServiceConnected invoked for non-open session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onServiceConnected(params));
+    }
+
+    /**
+     * @hide
+     */
+    private void executeCallback(@NonNull Runnable runnable) {
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            mExecutor.execute(runnable);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+}
diff --git a/framework/java/android/uwb/SessionHandle.aidl b/framework/java/android/uwb/SessionHandle.aidl
new file mode 100644
index 0000000..58a7dbb
--- /dev/null
+++ b/framework/java/android/uwb/SessionHandle.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+parcelable SessionHandle;
diff --git a/framework/java/android/uwb/SessionHandle.java b/framework/java/android/uwb/SessionHandle.java
new file mode 100644
index 0000000..b23f5ad
--- /dev/null
+++ b/framework/java/android/uwb/SessionHandle.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * @hide
+ */
+public final class SessionHandle implements Parcelable  {
+    private final int mId;
+
+    public SessionHandle(int id) {
+        mId = id;
+    }
+
+    protected SessionHandle(Parcel in) {
+        mId = in.readInt();
+    }
+
+    public static final Creator<SessionHandle> CREATOR = new Creator<SessionHandle>() {
+        @Override
+        public SessionHandle createFromParcel(Parcel in) {
+            return new SessionHandle(in);
+        }
+
+        @Override
+        public SessionHandle[] newArray(int size) {
+            return new SessionHandle[size];
+        }
+    };
+
+    public int getId() {
+        return mId;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mId);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof SessionHandle) {
+            SessionHandle other = (SessionHandle) obj;
+            return mId == other.mId;
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mId);
+    }
+
+    @Override
+    public String toString() {
+        return "SessionHandle [id=" + mId + "]";
+    }
+}
diff --git a/framework/java/android/uwb/StateChangeReason.aidl b/framework/java/android/uwb/StateChangeReason.aidl
new file mode 100644
index 0000000..28eaf9f
--- /dev/null
+++ b/framework/java/android/uwb/StateChangeReason.aidl
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+/**
+ * @hide
+ */
+@Backing(type="int")
+enum StateChangeReason {
+  /**
+   * The state changed for an unknown reason
+   */
+  UNKNOWN,
+
+  /**
+   * The adapter state changed because a session started.
+   */
+  SESSION_STARTED,
+
+
+  /**
+   * The adapter state changed because all sessions were closed.
+   */
+  ALL_SESSIONS_CLOSED,
+
+  /**
+   * The adapter state changed because of a device system change.
+   */
+  SYSTEM_POLICY,
+
+  /**
+   * Used to signal the first adapter state message after boot
+   */
+   SYSTEM_BOOT,
+}
+
diff --git a/framework/java/android/uwb/UwbAddress.aidl b/framework/java/android/uwb/UwbAddress.aidl
new file mode 100644
index 0000000..a202b1a
--- /dev/null
+++ b/framework/java/android/uwb/UwbAddress.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+parcelable UwbAddress;
diff --git a/framework/java/android/uwb/UwbAddress.java b/framework/java/android/uwb/UwbAddress.java
new file mode 100644
index 0000000..22883be
--- /dev/null
+++ b/framework/java/android/uwb/UwbAddress.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+
+/**
+ * A class representing a UWB address
+ *
+ * @hide
+ */
+@SystemApi
+public final class UwbAddress implements Parcelable {
+    public static final int SHORT_ADDRESS_BYTE_LENGTH = 2;
+    public static final int EXTENDED_ADDRESS_BYTE_LENGTH = 8;
+
+    private final byte[] mAddressBytes;
+
+    private UwbAddress(byte[] address) {
+        mAddressBytes = address;
+    }
+
+    /**
+     * Create a {@link UwbAddress} from a byte array.
+     *
+     * <p>If the provided array is {@link #SHORT_ADDRESS_BYTE_LENGTH} bytes, a short address is
+     * created. If the provided array is {@link #EXTENDED_ADDRESS_BYTE_LENGTH} bytes, then an
+     * extended address is created.
+     *
+     * @param address a byte array to convert to a {@link UwbAddress}
+     * @return a {@link UwbAddress} created from the input byte array
+     * @throws IllegalArgumentException when the length is not one of
+     *       {@link #SHORT_ADDRESS_BYTE_LENGTH} or {@link #EXTENDED_ADDRESS_BYTE_LENGTH} bytes
+     */
+    @NonNull
+    public static UwbAddress fromBytes(@NonNull byte[] address) {
+        if (address.length != SHORT_ADDRESS_BYTE_LENGTH
+                && address.length != EXTENDED_ADDRESS_BYTE_LENGTH) {
+            throw new IllegalArgumentException("Invalid UwbAddress length " + address.length);
+        }
+        return new UwbAddress(address);
+    }
+
+    /**
+     * Get the address as a byte array
+     *
+     * @return the byte representation of this {@link UwbAddress}
+     */
+    @NonNull
+    public byte[] toBytes() {
+        return mAddressBytes;
+    }
+
+    /**
+     * The length of the address in bytes
+     * <p>Possible values are {@link #SHORT_ADDRESS_BYTE_LENGTH} and
+     * {@link #EXTENDED_ADDRESS_BYTE_LENGTH}.
+     */
+    public int size() {
+        return mAddressBytes.length;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder("0x");
+        for (byte addressByte : mAddressBytes) {
+            builder.append(String.format("%02X", addressByte));
+        }
+        return builder.toString();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof UwbAddress) {
+            return Arrays.equals(mAddressBytes, ((UwbAddress) obj).toBytes());
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(mAddressBytes);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mAddressBytes.length);
+        dest.writeByteArray(mAddressBytes);
+    }
+
+    public static final @android.annotation.NonNull Creator<UwbAddress> CREATOR =
+            new Creator<UwbAddress>() {
+                @Override
+                public UwbAddress createFromParcel(Parcel in) {
+                    byte[] address = new byte[in.readInt()];
+                    in.readByteArray(address);
+                    return UwbAddress.fromBytes(address);
+                }
+
+                @Override
+                public UwbAddress[] newArray(int size) {
+                    return new UwbAddress[size];
+                }
+    };
+}
diff --git a/framework/java/android/uwb/UwbFrameworkInitializer.java b/framework/java/android/uwb/UwbFrameworkInitializer.java
new file mode 100644
index 0000000..f498034
--- /dev/null
+++ b/framework/java/android/uwb/UwbFrameworkInitializer.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package android.uwb;
+
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+
+/**
+ * Class for performing registration for Uwb service.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public class UwbFrameworkInitializer {
+    private UwbFrameworkInitializer() {}
+
+    /**
+     * Called by {@link SystemServiceRegistry}'s static initializer and registers UWB service
+     * to {@link Context}, so that {@link Context#getSystemService} can return them.
+     *
+     * @throws IllegalStateException if this is called from anywhere besides
+     * {@link SystemServiceRegistry}
+     */
+    public static void registerServiceWrappers() {
+        SystemServiceRegistry.registerContextAwareService(
+                Context.UWB_SERVICE,
+                UwbManager.class,
+                (context, serviceBinder) -> {
+                    IUwbAdapter adapter = IUwbAdapter.Stub.asInterface(serviceBinder);
+                    return new UwbManager(context, adapter);
+                }
+        );
+    }
+}
diff --git a/framework/java/android/uwb/UwbManager.java b/framework/java/android/uwb/UwbManager.java
new file mode 100644
index 0000000..fee2dbb
--- /dev/null
+++ b/framework/java/android/uwb/UwbManager.java
@@ -0,0 +1,884 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.Manifest.permission;
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.Binder;
+import android.os.CancellationSignal;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * This class provides a way to perform Ultra Wideband (UWB) operations such as querying the
+ * device's capabilities and determining the distance and angle between the local device and a
+ * remote device.
+ *
+ * <p>To get a {@link UwbManager}, call the <code>Context.getSystemService(UwbManager.class)</code>.
+ *
+ * <p> Note: This API surface uses opaque {@link PersistableBundle} params. These params are to be
+ * created using the provided UWB support library. The support library is present in this
+ * location on AOSP: <code>packages/modules/Uwb/service/support_lib/</code>
+ *
+ * @hide
+ */
+@SystemApi
+@SystemService(Context.UWB_SERVICE)
+public final class UwbManager {
+    private static final String TAG = "UwbManager";
+
+    private final Context mContext;
+    private final IUwbAdapter mUwbAdapter;
+    private final AdapterStateListener mAdapterStateListener;
+    private final RangingManager mRangingManager;
+    private final UwbVendorUciCallbackListener mUwbVendorUciCallbackListener;
+
+    /**
+     * Interface for receiving UWB adapter state changes
+     */
+    public interface AdapterStateCallback {
+        /**
+         * @hide
+         */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(value = {
+                STATE_CHANGED_REASON_SESSION_STARTED,
+                STATE_CHANGED_REASON_ALL_SESSIONS_CLOSED,
+                STATE_CHANGED_REASON_SYSTEM_POLICY,
+                STATE_CHANGED_REASON_SYSTEM_BOOT,
+                STATE_CHANGED_REASON_ERROR_UNKNOWN})
+        @interface StateChangedReason {}
+
+        /**
+         * @hide
+         */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(value = {
+                STATE_ENABLED_INACTIVE,
+                STATE_ENABLED_ACTIVE,
+                STATE_DISABLED})
+        @interface State {}
+
+        /**
+         * Indicates that the state change was due to opening of first UWB session
+         */
+        int STATE_CHANGED_REASON_SESSION_STARTED = 0;
+
+        /**
+         * Indicates that the state change was due to closure of all UWB sessions
+         */
+        int STATE_CHANGED_REASON_ALL_SESSIONS_CLOSED = 1;
+
+        /**
+         * Indicates that the state change was due to changes in system policy
+         */
+        int STATE_CHANGED_REASON_SYSTEM_POLICY = 2;
+
+        /**
+         * Indicates that the current state is due to a system boot
+         */
+        int STATE_CHANGED_REASON_SYSTEM_BOOT = 3;
+
+        /**
+         * Indicates that the state change was due to some unknown error
+         */
+        int STATE_CHANGED_REASON_ERROR_UNKNOWN = 4;
+
+        /**
+         * Indicates that UWB is disabled on device
+         */
+        int STATE_DISABLED = 0;
+        /**
+         * Indicates that UWB is enabled on device but has no active ranging sessions
+         */
+        int STATE_ENABLED_INACTIVE = 1;
+
+        /**
+         * Indicates that UWB is enabled and has active ranging session
+         */
+        int STATE_ENABLED_ACTIVE = 2;
+
+        /**
+         * Invoked when underlying UWB adapter's state is changed
+         * <p>Invoked with the adapter's current state after registering an
+         * {@link AdapterStateCallback} using
+         * {@link UwbManager#registerAdapterStateCallback(Executor, AdapterStateCallback)}.
+         *
+         * <p>Possible reasons for the state to change are
+         * {@link #STATE_CHANGED_REASON_SESSION_STARTED},
+         * {@link #STATE_CHANGED_REASON_ALL_SESSIONS_CLOSED},
+         * {@link #STATE_CHANGED_REASON_SYSTEM_POLICY},
+         * {@link #STATE_CHANGED_REASON_SYSTEM_BOOT},
+         * {@link #STATE_CHANGED_REASON_ERROR_UNKNOWN}.
+         *
+         * <p>Possible values for the UWB state are
+         * {@link #STATE_ENABLED_INACTIVE},
+         * {@link #STATE_ENABLED_ACTIVE},
+         * {@link #STATE_DISABLED}.
+         *
+         * @param state the UWB state; inactive, active or disabled
+         * @param reason the reason for the state change
+         */
+        void onStateChanged(@State int state, @StateChangedReason int reason);
+    }
+
+    /**
+     * Abstract class for receiving ADF provisioning state.
+     * Should be extended by applications and set when calling
+     * {@link UwbManager#provisionProfileAdfByScript(PersistableBundle, Executor,
+     * AdfProvisionStateCallback)}
+     */
+    public abstract static class AdfProvisionStateCallback {
+        private final AdfProvisionStateCallbackProxy mAdfProvisionStateCallbackProxy;
+
+        public AdfProvisionStateCallback() {
+            mAdfProvisionStateCallbackProxy = new AdfProvisionStateCallbackProxy();
+        }
+
+        /**
+         * @hide
+         */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(value = {
+                REASON_INVALID_OID,
+                REASON_SE_FAILURE,
+                REASON_UNKNOWN
+        })
+        @interface Reason { }
+
+        /**
+         * Indicates that the OID provided was not valid.
+         */
+        public static final int REASON_INVALID_OID = 1;
+
+        /**
+         * Indicates that there was some SE (secure element) failure while provisioning.
+         */
+        public static final int REASON_SE_FAILURE = 2;
+
+        /**
+         * No known reason for the failure.
+         */
+        public static final int REASON_UNKNOWN = 3;
+
+        /**
+         * Invoked when {@link UwbManager#provisionProfileAdfByScript(PersistableBundle, Executor,
+         * AdfProvisionStateCallback)} is successful.
+         *
+         * @param params protocol specific params that provide the caller with provisioning info
+         **/
+        public abstract void onProfileAdfsProvisioned(@NonNull PersistableBundle params);
+
+        /**
+         * Invoked when {@link UwbManager#provisionProfileAdfByScript(PersistableBundle, Executor,
+         * AdfProvisionStateCallback)} fails.
+         *
+         * @param reason Reason for failure
+         * @param params protocol specific parameters to indicate failure reason
+         */
+        public abstract void onProfileAdfsProvisionFailed(
+                @Reason int reason, @NonNull PersistableBundle params);
+
+        /*package*/
+        @NonNull
+        AdfProvisionStateCallbackProxy getProxy() {
+            return mAdfProvisionStateCallbackProxy;
+        }
+
+        private static class AdfProvisionStateCallbackProxy extends
+                IUwbAdfProvisionStateCallbacks.Stub {
+            private final Object mLock = new Object();
+            @Nullable
+            @GuardedBy("mLock")
+            private Executor mExecutor;
+            @Nullable
+            @GuardedBy("mLock")
+            private AdfProvisionStateCallback mCallback;
+
+            AdfProvisionStateCallbackProxy() {
+                mCallback = null;
+                mExecutor = null;
+            }
+
+            /*package*/ void initProxy(@NonNull Executor executor,
+                    @NonNull AdfProvisionStateCallback callback) {
+                synchronized (mLock) {
+                    mExecutor = executor;
+                    mCallback = callback;
+                }
+            }
+
+            /*package*/ void cleanUpProxy() {
+                synchronized (mLock) {
+                    mExecutor = null;
+                    mCallback = null;
+                }
+            }
+
+            @Override
+            public void onProfileAdfsProvisioned(@NonNull PersistableBundle params) {
+                Log.v(TAG, "AdfProvisionStateCallbackProxy: onProfileAdfsProvisioned : " + params);
+                AdfProvisionStateCallback callback;
+                Executor executor;
+                synchronized (mLock) {
+                    executor = mExecutor;
+                    callback = mCallback;
+                }
+                if (callback == null || executor == null) {
+                    return;
+                }
+                Binder.clearCallingIdentity();
+                executor.execute(() -> callback.onProfileAdfsProvisioned(params));
+                cleanUpProxy();
+            }
+
+            @Override
+            public void onProfileAdfsProvisionFailed(@AdfProvisionStateCallback.Reason int reason,
+                    @NonNull PersistableBundle params) {
+                Log.v(TAG, "AdfProvisionStateCallbackProxy: onProfileAdfsProvisionFailed : "
+                        + reason + ", " + params);
+                AdfProvisionStateCallback callback;
+                Executor executor;
+                synchronized (mLock) {
+                    executor = mExecutor;
+                    callback = mCallback;
+                }
+                if (callback == null || executor == null) {
+                    return;
+                }
+                Binder.clearCallingIdentity();
+                executor.execute(() -> callback.onProfileAdfsProvisionFailed(reason, params));
+                cleanUpProxy();
+            }
+        }
+    }
+
+    /**
+     * Interface for receiving vendor UCI responses and notifications.
+     */
+    public interface UwbVendorUciCallback {
+        /**
+         * Invoked when a vendor specific UCI response is received.
+         *
+         * @param gid Group ID of the command. This needs to be one of the vendor reserved GIDs from
+         *            the UCI specification.
+         * @param oid Opcode ID of the command. This is left to the OEM / vendor to decide.
+         * @param payload containing vendor Uci message payload.
+         */
+        void onVendorUciResponse(
+                @IntRange(from = 9, to = 15) int gid, int oid, @NonNull byte[] payload);
+
+        /**
+         * Invoked when a vendor specific UCI notification is received.
+         *
+         * @param gid Group ID of the command. This needs to be one of the vendor reserved GIDs from
+         *            the UCI specification.
+         * @param oid Opcode ID of the command. This is left to the OEM / vendor to decide.
+         * @param payload containing vendor Uci message payload.
+         */
+        void onVendorUciNotification(
+                @IntRange(from = 9, to = 15) int gid, int oid, @NonNull byte[] payload);
+    }
+
+    /**
+     * Use <code>Context.getSystemService(UwbManager.class)</code> to get an instance.
+     *
+     * @param ctx Context of the client.
+     * @param adapter an instance of an {@link android.uwb.IUwbAdapter}
+     * @hide
+     */
+    public UwbManager(@NonNull Context ctx, @NonNull IUwbAdapter adapter) {
+        mContext = ctx;
+        mUwbAdapter = adapter;
+        mAdapterStateListener = new AdapterStateListener(adapter);
+        mRangingManager = new RangingManager(adapter);
+        mUwbVendorUciCallbackListener = new UwbVendorUciCallbackListener(adapter);
+    }
+
+    /**
+     * Register an {@link AdapterStateCallback} to listen for UWB adapter state changes
+     * <p>The provided callback will be invoked by the given {@link Executor}.
+     *
+     * <p>When first registering a callback, the callbacks's
+     * {@link AdapterStateCallback#onStateChanged(int, int)} is immediately invoked to indicate
+     * the current state of the underlying UWB adapter with the most recent
+     * {@link AdapterStateCallback.StateChangedReason} that caused the change.
+     *
+     * @param executor an {@link Executor} to execute given callback
+     * @param callback user implementation of the {@link AdapterStateCallback}
+     */
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    public void registerAdapterStateCallback(@NonNull @CallbackExecutor Executor executor,
+            @NonNull AdapterStateCallback callback) {
+        mAdapterStateListener.register(executor, callback);
+    }
+
+    /**
+     * Unregister the specified {@link AdapterStateCallback}
+     * <p>The same {@link AdapterStateCallback} object used when calling
+     * {@link #registerAdapterStateCallback(Executor, AdapterStateCallback)} must be used.
+     *
+     * <p>Callbacks are automatically unregistered when application process goes away
+     *
+     * @param callback user implementation of the {@link AdapterStateCallback}
+     */
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    public void unregisterAdapterStateCallback(@NonNull AdapterStateCallback callback) {
+        mAdapterStateListener.unregister(callback);
+    }
+
+    /**
+     * Register an {@link UwbVendorUciCallback} to listen for UWB vendor responses and notifications
+     * <p>The provided callback will be invoked by the given {@link Executor}.
+     *
+     * <p>When first registering a callback, the callbacks's
+     * {@link UwbVendorUciCallback#onVendorUciCallBack(byte[])} is immediately invoked to
+     * notify the vendor notification.
+     *
+     * @param executor an {@link Executor} to execute given callback
+     * @param callback user implementation of the {@link UwbVendorUciCallback}
+     */
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    public void registerUwbVendorUciCallback(@NonNull @CallbackExecutor Executor executor,
+            @NonNull UwbVendorUciCallback callback) {
+        mUwbVendorUciCallbackListener.register(executor, callback);
+    }
+
+    /**
+     * Unregister the specified {@link UwbVendorUciCallback}
+     *
+     * <p>The same {@link UwbVendorUciCallback} object used when calling
+     * {@link #registerUwbVendorUciCallback(Executor, UwbVendorUciCallback)} must be used.
+     *
+     * <p>Callbacks are automatically unregistered when application process goes away
+     *
+     * @param callback user implementation of the {@link UwbVendorUciCallback}
+     */
+    public void unregisterUwbVendorUciCallback(@NonNull UwbVendorUciCallback callback) {
+        mUwbVendorUciCallbackListener.unregister(callback);
+    }
+
+    /**
+     * Get a {@link PersistableBundle} with the supported UWB protocols and parameters.
+     * <p>The {@link PersistableBundle} should be parsed using a support library
+     *
+     * <p>Android reserves the '^android.*' namespace</p>
+     *
+     * @return {@link PersistableBundle} of the device's supported UWB protocols and parameters
+     */
+    @NonNull
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    public PersistableBundle getSpecificationInfo() {
+        return getSpecificationInfoInternal(/* chipId= */ null);
+    }
+
+    /**
+     * Get a {@link PersistableBundle} with the supported UWB protocols and parameters.
+     *
+     * @see #getSpecificationInfo() if you don't need multi-HAL support
+     *
+     * @param chipId identifier of UWB chip for multi-HAL devices
+     *
+     * @return {@link PersistableBundle} of the device's supported UWB protocols and parameters
+     */
+    // TODO(b/205614701): Add documentation about how to find the relevant chipId
+    @NonNull
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    public PersistableBundle getSpecificationInfo(@NonNull String chipId) {
+        checkNotNull(chipId);
+        return getSpecificationInfoInternal(chipId);
+    }
+
+    private PersistableBundle getSpecificationInfoInternal(String chipId) {
+        try {
+            return mUwbAdapter.getSpecificationInfo(chipId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the timestamp resolution for events in nanoseconds
+     * <p>This value defines the maximum error of all timestamps for events reported to
+     * {@link RangingSession.Callback}.
+     *
+     * @return the timestamp resolution in nanoseconds
+     */
+    @SuppressLint("MethodNameUnits")
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    public long elapsedRealtimeResolutionNanos() {
+        return elapsedRealtimeResolutionNanosInternal(/* chipId= */ null);
+    }
+
+    /**
+     * Get the timestamp resolution for events in nanoseconds
+     *
+     * @see #elapsedRealtimeResolutionNanos() if you don't need multi-HAL support
+     *
+     * @param chipId identifier of UWB chip for multi-HAL devices
+     *
+     * @return the timestamp resolution in nanoseconds
+     */
+    @SuppressLint("MethodNameUnits")
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    public long elapsedRealtimeResolutionNanos(@NonNull String chipId) {
+        checkNotNull(chipId);
+        return elapsedRealtimeResolutionNanosInternal(chipId);
+    }
+
+    private long elapsedRealtimeResolutionNanosInternal(String chipId) {
+        try {
+            return mUwbAdapter.getTimestampResolutionNanos(chipId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Open a {@link RangingSession} with the given parameters
+     * <p>The {@link RangingSession.Callback#onOpened(RangingSession)} function is called with a
+     * {@link RangingSession} object used to control ranging when the session is successfully
+     * opened.
+     *
+     * if this session uses FIRA defined profile (not custom profile), this triggers:
+     *   - OOB discovery using service UUID
+     *   - OOB connection establishment after discovery for session params
+     *     negotiation.
+     *   - Secure element interactions needed for dynamic STS based session establishment.
+     *   - Setup the UWB session based on the parameters negotiated via OOB.
+     *
+     * <p>If a session cannot be opened, then
+     * {@link RangingSession.Callback#onClosed(int, PersistableBundle)} will be invoked with the
+     * appropriate {@link RangingSession.Callback.Reason}.
+     *
+     * <p>An open {@link RangingSession} will be automatically closed if client application process
+     * dies.
+     *
+     * <p>A UWB support library must be used in order to construct the {@code parameter}
+     * {@link PersistableBundle}.
+     *
+     * @param parameters the parameters that define the ranging session
+     * @param executor {@link Executor} to run callbacks
+     * @param callbacks {@link RangingSession.Callback} to associate with the
+     *                  {@link RangingSession} that is being opened.
+     *
+     * @return an {@link CancellationSignal} that is able to be used to cancel the opening of a
+     *         {@link RangingSession} that has been requested through {@link #openRangingSession}
+     *         but has not yet been made available by
+     *         {@link RangingSession.Callback#onOpened(RangingSession)}.
+     */
+    @NonNull
+    @RequiresPermission(allOf = {
+            permission.UWB_PRIVILEGED,
+            permission.UWB_RANGING
+    })
+    public CancellationSignal openRangingSession(@NonNull PersistableBundle parameters,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull RangingSession.Callback callbacks) {
+        return openRangingSessionInternal(parameters, executor, callbacks, /* chipId= */ null);
+    }
+
+    /**
+     * Open a {@link RangingSession} with the given parameters on a specific UWB subsystem
+     *
+     * @see #openRangingSession(PersistableBundle, Executor, RangingSession.Callback) if you don't
+     * need multi-HAL support
+     *
+     * @param parameters the parameters that define the ranging session
+     * @param executor {@link Executor} to run callbacks
+     * @param callbacks {@link RangingSession.Callback} to associate with the
+     *                  {@link RangingSession} that is being opened.
+     * @param chipId identifier of UWB chip for multi-HAL devices
+     *
+     * @return an {@link CancellationSignal} that is able to be used to cancel the opening of a
+     *         {@link RangingSession} that has been requested through {@link #openRangingSession}
+     *         but has not yet been made available by
+     *         {@link RangingSession.Callback#onOpened(RangingSession)}.
+     */
+    @NonNull
+    @RequiresPermission(allOf = {
+            permission.UWB_PRIVILEGED,
+            permission.UWB_RANGING
+    })
+    public CancellationSignal openRangingSession(@NonNull PersistableBundle parameters,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull RangingSession.Callback callbacks,
+            @SuppressLint("ListenerLast") @NonNull String chipId) {
+        checkNotNull(chipId);
+        return openRangingSessionInternal(parameters, executor, callbacks, chipId);
+    }
+
+    private CancellationSignal openRangingSessionInternal(PersistableBundle parameters,
+            Executor executor, RangingSession.Callback callbacks, String chipId) {
+        return mRangingManager.openSession(
+                mContext.getAttributionSource(), parameters, executor, callbacks, chipId);
+    }
+
+    /**
+     * Returns the current enabled/disabled state for UWB.
+     *
+     * Possible values are:
+     * AdapterStateCallback#STATE_DISABLED
+     * AdapterStateCallback#STATE_ENABLED_INACTIVE
+     * AdapterStateCallback#STATE_ENABLED_ACTIVE
+     *
+     * @return value representing current enabled/disabled state for UWB.
+     */
+    public @AdapterStateCallback.State int getAdapterState() {
+        return mAdapterStateListener.getAdapterState();
+    }
+
+    /**
+     * Whether UWB is enabled or disabled.
+     *
+     * <p>
+     * If disabled, this could indicate that either
+     * <li> User has toggled UWB off from settings, OR </li>
+     * <li> UWB subsystem has shut down due to a fatal error. </li>
+     * </p>
+     *
+     * @return true if enabled, false otherwise.
+     *
+     * @see #getAdapterState()
+     * @see #setUwbEnabled(boolean)
+     */
+    public boolean isUwbEnabled() {
+        int adapterState = getAdapterState();
+        return adapterState == AdapterStateCallback.STATE_ENABLED_ACTIVE
+                || adapterState == AdapterStateCallback.STATE_ENABLED_INACTIVE;
+
+    }
+
+    /**
+     * Disables or enables UWB by the user.
+     *
+     * If enabled any subsequent calls to
+     * {@link #openRangingSession(PersistableBundle, Executor, RangingSession.Callback)} will be
+     * allowed. If disabled, all active ranging sessions will be closed and subsequent calls to
+     * {@link #openRangingSession(PersistableBundle, Executor, RangingSession.Callback)} will be
+     * disallowed.
+     *
+     * @param enabled value representing intent to disable or enable UWB.
+     */
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    public void setUwbEnabled(boolean enabled) {
+        mAdapterStateListener.setEnabled(enabled);
+    }
+
+
+    /**
+     * Returns a list of UWB chip infos in a {@link PersistableBundle}.
+     *
+     * Callers can invoke methods on a specific UWB chip by passing its {@code chipId} to the
+     * method, which can be determined by calling:
+     * <pre>
+     * List<PersistableBundle> chipInfos = getChipInfos();
+     * for (PersistableBundle chipInfo : chipInfos) {
+     *     String chipId = ChipInfoParams.fromBundle(chipInfo).getChipId();
+     * }
+     * </pre>
+     *
+     * @return list of {@link PersistableBundle} containing info about UWB chips for a multi-HAL
+     * system, or a list of info for a single chip for a single HAL system.
+     */
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    @NonNull
+    public List<PersistableBundle> getChipInfos() {
+        try {
+            return mUwbAdapter.getChipInfos();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the default UWB chip identifier.
+     *
+     * If callers do not pass a specific {@code chipId} to UWB methods, then the method will be
+     * invoked on the default chip, which is determined at system initialization from a
+     * configuration file.
+     *
+     * @return default UWB chip identifier for a multi-HAL system, or the identifier of the only UWB
+     * chip in a single HAL system.
+     */
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    @NonNull
+    public String getDefaultChipId() {
+        try {
+            return mUwbAdapter.getDefaultChipId();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Register the UWB service profile.
+     * This profile instance is persisted by the platform until explicitly removed
+     * using {@link #removeServiceProfile(PersistableBundle)}
+     *
+     * @param parameters the parameters that define the service profile.
+     * @return Protocol specific params to be used as handle for triggering the profile.
+     */
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    @NonNull
+    public PersistableBundle addServiceProfile(@NonNull PersistableBundle parameters) {
+        try {
+            return mUwbAdapter.addServiceProfile(parameters);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Successfully removed the service profile.
+     */
+    public static final int REMOVE_SERVICE_PROFILE_SUCCESS = 0;
+
+    /**
+     * Failed to remove service since the service profile is unknown.
+     */
+    public static final int REMOVE_SERVICE_PROFILE_ERROR_UNKNOWN_SERVICE = 1;
+
+    /**
+     * Failed to remove service due to some internal error while processing the request.
+     */
+    public static final int REMOVE_SERVICE_PROFILE_ERROR_INTERNAL = 2;
+
+    /**
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            REMOVE_SERVICE_PROFILE_SUCCESS,
+            REMOVE_SERVICE_PROFILE_ERROR_UNKNOWN_SERVICE,
+            REMOVE_SERVICE_PROFILE_ERROR_INTERNAL
+    })
+    @interface RemoveServiceProfile {}
+
+    /**
+     * Remove the service profile registered with {@link #addServiceProfile} and
+     * all related resources.
+     *
+     * @param parameters the parameters that define the service profile.
+     *
+     * @return true if the service profile is removed, false otherwise.
+     */
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    public @RemoveServiceProfile int removeServiceProfile(@NonNull PersistableBundle parameters) {
+        try {
+            return mUwbAdapter.removeServiceProfile(parameters);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get all service profiles initialized with {@link #addServiceProfile}
+     *
+     * @return the parameters that define the service profiles.
+     */
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    @NonNull
+    public PersistableBundle getAllServiceProfiles() {
+        try {
+            return mUwbAdapter.getAllServiceProfiles();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the list of ADF (application defined file) provisioning authorities available for the UWB
+     * applet in SE (secure element).
+     *
+     * @param serviceProfileBundle Parameters representing the profile to use.
+     * @return The list of key information of ADF provisioning authority defined in FiRa
+     * CSML 8.2.2.7.2.4 and 8.2.2.14.4.1.2.
+     */
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    @NonNull
+    public PersistableBundle getAdfProvisioningAuthorities(
+            @NonNull PersistableBundle serviceProfileBundle) {
+        try {
+            return mUwbAdapter.getAdfProvisioningAuthorities(serviceProfileBundle);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get certificate information for the UWB applet in SE (secure element) that can be used to
+     * provision ADF (application defined file).
+     *
+     * @param serviceProfileBundle Parameters representing the profile to use.
+     * @return The Fira applet certificate information defined in FiRa CSML 7.3.4.3 and
+     * 8.2.2.14.4.1.1
+     */
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    @NonNull
+    public PersistableBundle getAdfCertificateInfo(
+            @NonNull PersistableBundle serviceProfileBundle) {
+        try {
+            return mUwbAdapter.getAdfCertificateAndInfo(serviceProfileBundle);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Mechanism to provision ADFs (application defined file) in the UWB applet present in SE
+     * (secure element) for a profile instance.
+     *
+     * @param serviceProfileBundle Parameters representing the profile to use.
+     * @param executor an {@link Executor} to execute given callback
+     * @param callback user implementation of the {@link AdapterStateCallback}
+     */
+    public void provisionProfileAdfByScript(@NonNull PersistableBundle serviceProfileBundle,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull AdfProvisionStateCallback callback) {
+        if (executor == null) throw new IllegalArgumentException("executor must not be null");
+        if (callback == null) throw new IllegalArgumentException("callback must not be null");
+        AdfProvisionStateCallback.AdfProvisionStateCallbackProxy proxy = callback.getProxy();
+        proxy.initProxy(executor, callback);
+        try {
+            mUwbAdapter.provisionProfileAdfByScript(serviceProfileBundle, proxy);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Successfully removed the profile ADF.
+     */
+    public static final int REMOVE_PROFILE_ADF_SUCCESS = 0;
+
+    /**
+     * Failed to remove ADF since the service profile is unknown.
+     */
+    public static final int REMOVE_PROFILE_ADF_ERROR_UNKNOWN_SERVICE = 1;
+
+    /**
+     * Failed to remove ADF due to some internal error while processing the request.
+     */
+    public static final int REMOVE_PROFILE_ADF_ERROR_INTERNAL = 2;
+
+    /**
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            REMOVE_PROFILE_ADF_SUCCESS,
+            REMOVE_PROFILE_ADF_ERROR_UNKNOWN_SERVICE,
+            REMOVE_PROFILE_ADF_ERROR_INTERNAL
+    })
+    @interface RemoveProfileAdf {}
+
+    /**
+     * Remove the ADF (application defined file) provisioned by {@link #provisionProfileAdfByScript}
+     *
+     * @param serviceProfileBundle Parameters representing the profile to use.
+     * @return true if the ADF is removed, false otherwise.
+     */
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    public @RemoveProfileAdf int removeProfileAdf(@NonNull PersistableBundle serviceProfileBundle) {
+        try {
+            return mUwbAdapter.removeProfileAdf(serviceProfileBundle);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Successfully sent the UCI message.
+     */
+    public static final int SEND_VENDOR_UCI_SUCCESS = 0;
+
+    /**
+     * Failed to send the UCI message because of an error returned from the HAL interface.
+     */
+    public static final int SEND_VENDOR_UCI_ERROR_HW = 1;
+
+    /**
+     * Failed to send the UCI message since UWB is toggled off.
+     */
+    public static final int SEND_VENDOR_UCI_ERROR_OFF = 2;
+
+    /**
+     * Failed to send the UCI message since UWB UCI command is malformed.
+     * GID.
+     */
+    public static final int SEND_VENDOR_UCI_ERROR_INVALID_ARGS = 3;
+
+    /**
+     * Failed to send the UCI message since UWB GID used is invalid.
+     */
+    public static final int SEND_VENDOR_UCI_ERROR_INVALID_GID = 4;
+
+    /**
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            SEND_VENDOR_UCI_SUCCESS,
+            SEND_VENDOR_UCI_ERROR_HW,
+            SEND_VENDOR_UCI_ERROR_OFF,
+            SEND_VENDOR_UCI_ERROR_INVALID_ARGS,
+            SEND_VENDOR_UCI_ERROR_INVALID_GID,
+    })
+    @interface SendVendorUciStatus {}
+
+    /**
+     * Send Vendor specific Uci Messages.
+     *
+     * The format of the UCI messages are defined in the UCI specification. The platform is
+     * responsible for fragmenting the payload if necessary.
+     *
+     * @param gid Group ID of the command. This needs to be one of the vendor reserved GIDs from
+     *            the UCI specification.
+     * @param oid Opcode ID of the command. This is left to the OEM / vendor to decide.
+     * @param payload containing vendor Uci message payload.
+     */
+    @NonNull
+    @RequiresPermission(permission.UWB_PRIVILEGED)
+    public @SendVendorUciStatus int sendVendorUciMessage(
+            @IntRange(from = 9, to = 15) int gid, int oid, @NonNull byte[] payload) {
+        try {
+            return mUwbAdapter.sendVendorUciMessage(gid, oid, payload);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/framework/java/android/uwb/UwbVendorUciCallbackListener.java b/framework/java/android/uwb/UwbVendorUciCallbackListener.java
new file mode 100644
index 0000000..3b1f699
--- /dev/null
+++ b/framework/java/android/uwb/UwbVendorUciCallbackListener.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 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.
+ */
+
+package android.uwb;
+import android.annotation.NonNull;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.uwb.UwbManager.UwbVendorUciCallback;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ */
+public final class UwbVendorUciCallbackListener extends IUwbVendorUciCallback.Stub{
+    private static final String TAG = "Uwb.UwbVendorUciCallbacks";
+    private final IUwbAdapter mAdapter;
+    private boolean mIsRegistered = false;
+    private final Map<UwbVendorUciCallback, Executor> mCallbackMap = new HashMap<>();
+
+    public UwbVendorUciCallbackListener(@NonNull IUwbAdapter adapter) {
+        mAdapter = adapter;
+    }
+
+    public void register(@NonNull Executor executor, @NonNull UwbVendorUciCallback callback) {
+        synchronized (this) {
+            if (mCallbackMap.containsKey(callback)) {
+                return;
+            }
+            mCallbackMap.put(callback, executor);
+            if (!mIsRegistered) {
+                try {
+                    mAdapter.registerVendorExtensionCallback(this);
+                    mIsRegistered = true;
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Failed to register adapter state callback");
+                    throw e.rethrowFromSystemServer();
+                }
+            }
+        }
+    }
+    public void unregister(@NonNull UwbVendorUciCallback callback) {
+        synchronized (this) {
+            if (!mCallbackMap.containsKey(callback)) {
+                return;
+            }
+            mCallbackMap.remove(callback);
+            if (mCallbackMap.isEmpty() && mIsRegistered) {
+                try {
+                    mAdapter.unregisterVendorExtensionCallback(this);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Failed to unregister AdapterStateCallback with service");
+                    throw e.rethrowFromSystemServer();
+                }
+                mIsRegistered = false;
+            }
+        }
+    }
+
+    @Override
+    public void onVendorResponseReceived(int gid, int oid, @NonNull byte[] payload)
+            throws RemoteException {
+        synchronized (this) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                for (UwbVendorUciCallback callback : mCallbackMap.keySet()) {
+                    Executor executor = mCallbackMap.get(callback);
+                    executor.execute(() -> callback.onVendorUciResponse(gid, oid, payload));
+                }
+            } catch (RuntimeException ex) {
+                throw ex;
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+
+    @Override
+    public void onVendorNotificationReceived(int gid, int oid, @NonNull byte[] payload)
+            throws RemoteException {
+        synchronized (this) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                for (UwbVendorUciCallback callback : mCallbackMap.keySet()) {
+                    Executor executor = mCallbackMap.get(callback);
+                    executor.execute(() -> callback.onVendorUciNotification(gid, oid, payload));
+                }
+            } catch (RuntimeException ex) {
+                throw ex;
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+}
diff --git a/framework/tests/Android.bp b/framework/tests/Android.bp
new file mode 100644
index 0000000..63ab379
--- /dev/null
+++ b/framework/tests/Android.bp
@@ -0,0 +1,62 @@
+// Copyright (C) 2021 The Android 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.
+
+// Make test APK
+// ============================================================
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+    name: "framework-uwb-test-util-srcs",
+    srcs: [
+        "src/android/uwb/UwbTestUtils.java",
+    ],
+}
+
+android_test {
+    name: "FrameworkUwbTests",
+
+    defaults: ["framework-uwb-test-defaults"],
+
+    min_sdk_version: "Tiramisu",
+    target_sdk_version: "Tiramisu",
+
+    srcs: ["**/*.java"],
+
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "frameworks-base-testutils",
+        "guava",
+        "mockito-target-minus-junit4",
+        "truth-prebuilt",
+    ],
+
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+
+    test_suites: [
+        "general-tests",
+        "mts-uwb",
+    ],
+
+    // static libs used by both framework-uwb & FrameworksUwbApiTests. Need to rename test usage
+    // to a different package name to prevent conflict with the copy in production code.
+    jarjar_rules: "test-jarjar-rules.txt",
+}
diff --git a/framework/tests/AndroidManifest.xml b/framework/tests/AndroidManifest.xml
new file mode 100644
index 0000000..adc3e46
--- /dev/null
+++ b/framework/tests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.uwb.test">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <!-- This is a self-instrumenting test package. -->
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.uwb.test"
+                     android:label="UWB Manager Tests">
+    </instrumentation>
+
+</manifest>
+
diff --git a/framework/tests/AndroidTest.xml b/framework/tests/AndroidTest.xml
new file mode 100644
index 0000000..0e96683
--- /dev/null
+++ b/framework/tests/AndroidTest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 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.
+-->
+<configuration description="Config for UWB Manager test cases">
+    <option name="test-suite-tag" value="apct"/>
+    <option name="test-suite-tag" value="apct-instrumentation"/>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="FrameworkUwbTests.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct"/>
+    <option name="test-tag" value="FrameworkUwbTests"/>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.uwb.test" />
+        <option name="hidden-api-checks" value="false"/>
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/>
+    </test>
+
+    <!-- Only run FrameworksUwbTests in MTS if the Uwb Mainline module is installed. -->
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.uwb" />
+    </object>
+</configuration>
diff --git a/framework/tests/src/android/uwb/AdapterStateListenerTest.java b/framework/tests/src/android/uwb/AdapterStateListenerTest.java
new file mode 100644
index 0000000..4cad535
--- /dev/null
+++ b/framework/tests/src/android/uwb/AdapterStateListenerTest.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+
+import android.os.RemoteException;
+import android.uwb.UwbManager.AdapterStateCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Test of {@link AdapterStateListener}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AdapterStateListenerTest {
+
+    IUwbAdapter mUwbAdapter = mock(IUwbAdapter.class);
+
+    Answer mRegisterSuccessAnswer = new Answer() {
+        public Object answer(InvocationOnMock invocation) {
+            Object[] args = invocation.getArguments();
+            IUwbAdapterStateCallbacks cb = (IUwbAdapterStateCallbacks) args[0];
+            try {
+                cb.onAdapterStateChanged(AdapterState.STATE_DISABLED, StateChangeReason.UNKNOWN);
+            } catch (RemoteException e) {
+                // Nothing to do
+            }
+            return new Object();
+        }
+    };
+
+    Throwable mThrowRemoteException = new RemoteException("RemoteException");
+
+    private static Executor getExecutor() {
+        return new Executor() {
+            @Override
+            public void execute(Runnable command) {
+                command.run();
+            }
+        };
+    }
+
+    private static void verifyCallbackStateChangedInvoked(
+            AdapterStateCallback callback, int numTimes) {
+        verify(callback, times(numTimes)).onStateChanged(anyInt(), anyInt());
+    }
+
+    @Test
+    public void testRegister_RegisterUnregister() throws RemoteException {
+        doAnswer(mRegisterSuccessAnswer).when(mUwbAdapter).registerAdapterStateCallbacks(any());
+
+        AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
+        AdapterStateCallback callback1 = mock(AdapterStateCallback.class);
+        AdapterStateCallback callback2 = mock(AdapterStateCallback.class);
+
+        // Verify that the adapter state listener registered with the UWB Adapter
+        adapterStateListener.register(getExecutor(), callback1);
+        verify(mUwbAdapter, times(1)).registerAdapterStateCallbacks(any());
+        verifyCallbackStateChangedInvoked(callback1, 1);
+        verifyCallbackStateChangedInvoked(callback2, 0);
+
+        // Register a second client and no new call to UWB Adapter
+        adapterStateListener.register(getExecutor(), callback2);
+        verify(mUwbAdapter, times(1)).registerAdapterStateCallbacks(any());
+        verifyCallbackStateChangedInvoked(callback1, 1);
+        verifyCallbackStateChangedInvoked(callback2, 1);
+
+        // Unregister first callback
+        adapterStateListener.unregister(callback1);
+        verify(mUwbAdapter, times(1)).registerAdapterStateCallbacks(any());
+        verify(mUwbAdapter, times(0)).unregisterAdapterStateCallbacks(any());
+        verifyCallbackStateChangedInvoked(callback1, 1);
+        verifyCallbackStateChangedInvoked(callback2, 1);
+
+        // Unregister second callback
+        adapterStateListener.unregister(callback2);
+        verify(mUwbAdapter, times(1)).registerAdapterStateCallbacks(any());
+        verify(mUwbAdapter, times(1)).unregisterAdapterStateCallbacks(any());
+        verifyCallbackStateChangedInvoked(callback1, 1);
+        verifyCallbackStateChangedInvoked(callback2, 1);
+    }
+
+    @Test
+    public void testRegister_RegisterSameCallbackTwice() throws RemoteException {
+        AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
+        AdapterStateCallback callback = mock(AdapterStateCallback.class);
+        doAnswer(mRegisterSuccessAnswer).when(mUwbAdapter).registerAdapterStateCallbacks(any());
+
+        adapterStateListener.register(getExecutor(), callback);
+        verifyCallbackStateChangedInvoked(callback, 1);
+
+        adapterStateListener.register(getExecutor(), callback);
+        verifyCallbackStateChangedInvoked(callback, 1);
+
+        // Invoke a state change and ensure the callback is only called once
+        adapterStateListener.onAdapterStateChanged(AdapterState.STATE_DISABLED,
+                StateChangeReason.UNKNOWN);
+        verifyCallbackStateChangedInvoked(callback, 2);
+    }
+
+    @Test
+    public void testCallback_RunViaExecutor_Success() throws RemoteException {
+        // Verify that the callbacks are invoked on the executor when successful
+        doAnswer(mRegisterSuccessAnswer).when(mUwbAdapter).registerAdapterStateCallbacks(any());
+        runViaExecutor();
+    }
+
+    private void runViaExecutor() {
+        AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
+        AdapterStateCallback callback = mock(AdapterStateCallback.class);
+
+        Executor executor = mock(Executor.class);
+
+        // Do not run commands received and ensure that the callback is not invoked
+        doAnswer(new ExecutorAnswer(false)).when(executor).execute(any());
+        adapterStateListener.register(executor, callback);
+        verify(executor, times(1)).execute(any());
+        verifyCallbackStateChangedInvoked(callback, 0);
+
+        // Manually invoke the callback and ensure callback is not invoked
+        adapterStateListener.onAdapterStateChanged(AdapterState.STATE_DISABLED,
+                StateChangeReason.UNKNOWN);
+        verify(executor, times(2)).execute(any());
+        verifyCallbackStateChangedInvoked(callback, 0);
+
+        // Run the command that the executor receives
+        doAnswer(new ExecutorAnswer(true)).when(executor).execute(any());
+        adapterStateListener.onAdapterStateChanged(AdapterState.STATE_DISABLED,
+                StateChangeReason.UNKNOWN);
+        verify(executor, times(3)).execute(any());
+        verifyCallbackStateChangedInvoked(callback, 1);
+    }
+
+    class ExecutorAnswer implements Answer {
+
+        final boolean mShouldRun;
+        ExecutorAnswer(boolean shouldRun) {
+            mShouldRun = shouldRun;
+        }
+
+        @Override
+        public Object answer(InvocationOnMock invocation) throws Throwable {
+            if (mShouldRun) {
+                ((Runnable) invocation.getArgument(0)).run();
+            }
+            return null;
+        }
+    }
+
+    @Test
+    public void testNotify_AllCallbacksNotified() throws RemoteException {
+        doAnswer(mRegisterSuccessAnswer).when(mUwbAdapter).registerAdapterStateCallbacks(any());
+
+        AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
+        List<AdapterStateCallback> callbacks = new ArrayList<>();
+        for (int i = 0; i < 10; i++) {
+            AdapterStateCallback callback = mock(AdapterStateCallback.class);
+            adapterStateListener.register(getExecutor(), callback);
+            callbacks.add(callback);
+        }
+
+            // Ensure every callback got the initial state
+        for (AdapterStateCallback callback : callbacks) {
+            verifyCallbackStateChangedInvoked(callback, 1);
+        }
+
+        // Invoke a state change and ensure all callbacks are invoked
+        adapterStateListener.onAdapterStateChanged(AdapterState.STATE_DISABLED,
+                StateChangeReason.ALL_SESSIONS_CLOSED);
+        for (AdapterStateCallback callback : callbacks) {
+            verifyCallbackStateChangedInvoked(callback, 2);
+        }
+    }
+
+    @Test
+    public void testStateChange_CorrectValue() {
+        AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
+
+        AdapterStateCallback callback = mock(AdapterStateCallback.class);
+
+        adapterStateListener.register(getExecutor(), callback);
+
+        runStateChangeValue(StateChangeReason.ALL_SESSIONS_CLOSED,
+                AdapterState.STATE_ENABLED_INACTIVE,
+                AdapterStateCallback.STATE_CHANGED_REASON_ALL_SESSIONS_CLOSED,
+                AdapterStateCallback.STATE_ENABLED_INACTIVE);
+
+        runStateChangeValue(StateChangeReason.SESSION_STARTED, AdapterState.STATE_ENABLED_ACTIVE,
+                AdapterStateCallback.STATE_CHANGED_REASON_SESSION_STARTED,
+                AdapterStateCallback.STATE_ENABLED_ACTIVE);
+
+        runStateChangeValue(StateChangeReason.SYSTEM_BOOT, AdapterState.STATE_DISABLED,
+                AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_BOOT,
+                AdapterStateCallback.STATE_DISABLED);
+
+        runStateChangeValue(StateChangeReason.SYSTEM_POLICY, AdapterState.STATE_DISABLED,
+                AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_POLICY,
+                AdapterStateCallback.STATE_DISABLED);
+
+        runStateChangeValue(StateChangeReason.UNKNOWN, AdapterState.STATE_DISABLED,
+                AdapterStateCallback.STATE_CHANGED_REASON_ERROR_UNKNOWN,
+                AdapterStateCallback.STATE_DISABLED);
+    }
+
+    private void runStateChangeValue(@StateChangeReason int reasonIn, @AdapterState int stateIn,
+            @AdapterStateCallback.StateChangedReason int reasonOut,
+            @AdapterStateCallback.State int stateOut) {
+        AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
+        AdapterStateCallback callback = mock(AdapterStateCallback.class);
+        adapterStateListener.register(getExecutor(), callback);
+
+        adapterStateListener.onAdapterStateChanged(stateIn, reasonIn);
+        verify(callback, times(1)).onStateChanged(stateOut, reasonOut);
+    }
+
+    @Test
+    public void testStateChange_FirstRegisterGetsCorrectState() throws RemoteException {
+        AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
+        AdapterStateCallback callback = mock(AdapterStateCallback.class);
+
+        Answer registerAnswer = new Answer() {
+            public Object answer(InvocationOnMock invocation) {
+                Object[] args = invocation.getArguments();
+                IUwbAdapterStateCallbacks cb = (IUwbAdapterStateCallbacks) args[0];
+                try {
+                    cb.onAdapterStateChanged(AdapterState.STATE_ENABLED_ACTIVE,
+                            StateChangeReason.SESSION_STARTED);
+                } catch (RemoteException e) {
+                    // Nothing to do
+                }
+                return new Object();
+            }
+        };
+
+        doAnswer(registerAnswer).when(mUwbAdapter).registerAdapterStateCallbacks(any());
+
+        adapterStateListener.register(getExecutor(), callback);
+        verify(callback).onStateChanged(AdapterStateCallback.STATE_ENABLED_ACTIVE,
+                AdapterStateCallback.STATE_CHANGED_REASON_SESSION_STARTED);
+    }
+
+    @Test
+    public void testStateChange_SecondRegisterGetsCorrectState() {
+        AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
+        AdapterStateCallback callback1 = mock(AdapterStateCallback.class);
+        AdapterStateCallback callback2 = mock(AdapterStateCallback.class);
+
+        adapterStateListener.register(getExecutor(), callback1);
+        adapterStateListener.onAdapterStateChanged(AdapterState.STATE_ENABLED_ACTIVE,
+                StateChangeReason.SYSTEM_BOOT);
+
+        adapterStateListener.register(getExecutor(), callback2);
+        verify(callback2).onStateChanged(AdapterStateCallback.STATE_ENABLED_ACTIVE,
+                AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_BOOT);
+    }
+}
diff --git a/framework/tests/src/android/uwb/RangingManagerTest.java b/framework/tests/src/android/uwb/RangingManagerTest.java
new file mode 100644
index 0000000..466f85d
--- /dev/null
+++ b/framework/tests/src/android/uwb/RangingManagerTest.java
@@ -0,0 +1,509 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.AttributionSource;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Test of {@link RangingManager}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RangingManagerTest {
+
+    private static final Executor EXECUTOR = UwbTestUtils.getExecutor();
+    private static final PersistableBundle PARAMS = new PersistableBundle();
+    private static final @RangingChangeReason int REASON = RangingChangeReason.UNKNOWN;
+    private static final UwbAddress ADDRESS = UwbAddress.fromBytes(new byte[] {0x0, 0x1});
+    private static final byte[] DATA = new byte[] {0x0, 0x1};
+    private static final int UID = 343453;
+    private static final String PACKAGE_NAME = "com.uwb.test";
+    private static final AttributionSource ATTRIBUTION_SOURCE =
+            new AttributionSource.Builder(UID).setPackageName(PACKAGE_NAME).build();
+    private static final String VALID_CHIP_ID = "validChipId";
+
+    @Test
+    public void testOpenSession_OpenRangingInvoked() throws RemoteException {
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingManager rangingManager = new RangingManager(adapter);
+        RangingSession.Callback callback = mock(RangingSession.Callback.class);
+        rangingManager.openSession(
+                ATTRIBUTION_SOURCE, PARAMS, EXECUTOR, callback, /* chipIds= */ null);
+        verify(adapter, times(1))
+                .openRanging(eq(ATTRIBUTION_SOURCE), any(), any(), any(), eq(/* chipId= */ null));
+    }
+
+    @Test
+    public void testOpenSession_validChipId_OpenRangingInvoked() throws RemoteException {
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        when(adapter.getChipIds()).thenReturn(List.of(VALID_CHIP_ID));
+        RangingManager rangingManager = new RangingManager(adapter);
+        RangingSession.Callback callback = mock(RangingSession.Callback.class);
+        rangingManager.openSession(ATTRIBUTION_SOURCE, PARAMS, EXECUTOR, callback, VALID_CHIP_ID);
+        verify(adapter, times(1))
+                .openRanging(eq(ATTRIBUTION_SOURCE), any(), any(), any(), eq(VALID_CHIP_ID));
+    }
+
+    @Test
+    public void testOpenSession_validChipId_RuntimeException() throws RemoteException {
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        doThrow(new RemoteException())
+                .when(adapter)
+                .openRanging(eq(ATTRIBUTION_SOURCE), any(), any(), any(), eq(VALID_CHIP_ID));
+        Mockito.when(adapter.getChipIds()).thenReturn(List.of(VALID_CHIP_ID));
+        RangingManager rangingManager = new RangingManager(adapter);
+        RangingSession.Callback callback = mock(RangingSession.Callback.class);
+        assertThrows(
+                RuntimeException.class,
+                () ->
+                        rangingManager.openSession(
+                                ATTRIBUTION_SOURCE, PARAMS, EXECUTOR, callback, VALID_CHIP_ID));
+    }
+
+    @Test
+    public void testOpenSession_invalidChipId_IllegalArgumentException() throws RemoteException {
+        String invalidChipId = "invalidChipId";
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        Mockito.when(adapter.getChipIds()).thenReturn(List.of(VALID_CHIP_ID));
+        RangingManager rangingManager = new RangingManager(adapter);
+        RangingSession.Callback callback = mock(RangingSession.Callback.class);
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        rangingManager.openSession(
+                                ATTRIBUTION_SOURCE, PARAMS, EXECUTOR, callback, invalidChipId));
+        verify(adapter, times(0))
+                .openRanging(eq(ATTRIBUTION_SOURCE), any(), any(), any(), eq(invalidChipId));
+    }
+
+    @Test
+    public void testOnRangingOpened_InvalidSessionHandle() throws RemoteException {
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingManager rangingManager = new RangingManager(adapter);
+        RangingSession.Callback callback = mock(RangingSession.Callback.class);
+
+        rangingManager.onRangingOpened(new SessionHandle(2));
+        verify(callback, times(0)).onOpened(any());
+    }
+
+    @Test
+    public void testOnRangingOpened_MultipleSessionsRegistered() throws RemoteException {
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession.Callback callback1 = mock(RangingSession.Callback.class);
+        RangingSession.Callback callback2 = mock(RangingSession.Callback.class);
+        ArgumentCaptor<SessionHandle> sessionHandleCaptor =
+                ArgumentCaptor.forClass(SessionHandle.class);
+
+        RangingManager rangingManager = new RangingManager(adapter);
+        rangingManager.openSession(
+                ATTRIBUTION_SOURCE, PARAMS, EXECUTOR, callback1, /* chipIds= */ null);
+        verify(adapter, times(1))
+                .openRanging(
+                        eq(ATTRIBUTION_SOURCE),
+                        sessionHandleCaptor.capture(),
+                        any(),
+                        any(),
+                        eq(/* chipId= */ null));
+        SessionHandle sessionHandle1 = sessionHandleCaptor.getValue();
+
+        rangingManager.openSession(
+                ATTRIBUTION_SOURCE, PARAMS, EXECUTOR, callback2, /* chipIds= */ null);
+        verify(adapter, times(2))
+                .openRanging(
+                        eq(ATTRIBUTION_SOURCE),
+                        sessionHandleCaptor.capture(),
+                        any(),
+                        any(),
+                        eq(/* chipId= */ null));
+        SessionHandle sessionHandle2 = sessionHandleCaptor.getValue();
+
+        rangingManager.onRangingOpened(sessionHandle1);
+        verify(callback1, times(1)).onOpened(any());
+        verify(callback2, times(0)).onOpened(any());
+
+        rangingManager.onRangingOpened(sessionHandle2);
+        verify(callback1, times(1)).onOpened(any());
+        verify(callback2, times(1)).onOpened(any());
+    }
+
+    @Test
+    public void testCorrectCallbackInvoked() throws RemoteException {
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingManager rangingManager = new RangingManager(adapter);
+        RangingSession.Callback callback = mock(RangingSession.Callback.class);
+
+        ArgumentCaptor<SessionHandle> sessionHandleCaptor =
+                ArgumentCaptor.forClass(SessionHandle.class);
+
+        rangingManager.openSession(
+                ATTRIBUTION_SOURCE, PARAMS, EXECUTOR, callback, /* chipIds= */ null);
+        verify(adapter, times(1))
+                .openRanging(
+                        eq(ATTRIBUTION_SOURCE),
+                        sessionHandleCaptor.capture(),
+                        any(),
+                        any(),
+                        eq(/* chipId= */ null));
+        SessionHandle handle = sessionHandleCaptor.getValue();
+
+        rangingManager.onRangingOpened(handle);
+        verify(callback, times(1)).onOpened(any());
+
+        rangingManager.onRangingStarted(handle, PARAMS);
+        verify(callback, times(1)).onStarted(eq(PARAMS));
+
+        rangingManager.onRangingStartFailed(handle, REASON, PARAMS);
+        verify(callback, times(1)).onStartFailed(eq(REASON), eq(PARAMS));
+
+        RangingReport report = UwbTestUtils.getRangingReports(1);
+        rangingManager.onRangingResult(handle, report);
+        verify(callback, times(1)).onReportReceived(eq(report));
+
+        rangingManager.onRangingReconfigured(handle, PARAMS);
+        verify(callback, times(1)).onReconfigured(eq(PARAMS));
+
+        rangingManager.onRangingReconfigureFailed(handle, REASON, PARAMS);
+        verify(callback, times(1)).onReconfigureFailed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onRangingStopped(handle, REASON, PARAMS);
+        verify(callback, times(1)).onStopped(eq(REASON), eq(PARAMS));
+
+        rangingManager.onRangingStopFailed(handle, REASON, PARAMS);
+        verify(callback, times(1)).onStopFailed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onControleeAdded(handle, PARAMS);
+        verify(callback, times(1)).onControleeAdded(eq(PARAMS));
+
+        rangingManager.onControleeAddFailed(handle, REASON, PARAMS);
+        verify(callback, times(1)).onControleeAddFailed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onControleeRemoved(handle, PARAMS);
+        verify(callback, times(1)).onControleeRemoved(eq(PARAMS));
+
+        rangingManager.onControleeRemoveFailed(handle, REASON, PARAMS);
+        verify(callback, times(1)).onControleeRemoveFailed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onRangingPaused(handle, PARAMS);
+        verify(callback, times(1)).onPaused(eq(PARAMS));
+
+        rangingManager.onRangingPauseFailed(handle, REASON, PARAMS);
+        verify(callback, times(1)).onPauseFailed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onRangingResumed(handle, PARAMS);
+        verify(callback, times(1)).onResumed(eq(PARAMS));
+
+        rangingManager.onRangingResumeFailed(handle, REASON, PARAMS);
+        verify(callback, times(1)).onResumeFailed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onDataSent(handle, ADDRESS, PARAMS);
+        verify(callback, times(1)).onDataSent(eq(ADDRESS), eq(PARAMS));
+
+        rangingManager.onDataSendFailed(handle, ADDRESS, REASON, PARAMS);
+        verify(callback, times(1)).onDataSendFailed(eq(ADDRESS), eq(REASON), eq(PARAMS));
+
+        rangingManager.onDataReceived(handle, ADDRESS, PARAMS, DATA);
+        verify(callback, times(1)).onDataReceived(eq(ADDRESS), eq(PARAMS), eq(DATA));
+
+        rangingManager.onDataReceiveFailed(handle, ADDRESS, REASON, PARAMS);
+        verify(callback, times(1)).onDataReceiveFailed(eq(ADDRESS), eq(REASON), eq(PARAMS));
+
+        rangingManager.onServiceDiscovered(handle, PARAMS);
+        verify(callback, times(1)).onServiceDiscovered(eq(PARAMS));
+
+        rangingManager.onServiceConnected(handle, PARAMS);
+        verify(callback, times(1)).onServiceConnected(eq(PARAMS));
+
+        rangingManager.onRangingClosed(handle, REASON, PARAMS);
+        verify(callback, times(1)).onClosed(eq(REASON), eq(PARAMS));
+    }
+
+    @Test
+    public void testNoCallbackInvoked_sessionClosed() throws RemoteException {
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingManager rangingManager = new RangingManager(adapter);
+        RangingSession.Callback callback = mock(RangingSession.Callback.class);
+
+        ArgumentCaptor<SessionHandle> sessionHandleCaptor =
+                ArgumentCaptor.forClass(SessionHandle.class);
+
+        rangingManager.openSession(
+                ATTRIBUTION_SOURCE, PARAMS, EXECUTOR, callback, /* chipIds= */ null);
+        verify(adapter, times(1))
+                .openRanging(
+                        eq(ATTRIBUTION_SOURCE),
+                        sessionHandleCaptor.capture(),
+                        any(),
+                        any(),
+                        eq(/* chipId= */ null));
+        SessionHandle handle = sessionHandleCaptor.getValue();
+        rangingManager.onRangingClosed(handle, REASON, PARAMS);
+        verify(callback, times(1)).onClosed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onRangingOpened(handle);
+        verify(callback, never()).onOpened(any());
+
+        rangingManager.onRangingStarted(handle, PARAMS);
+        verify(callback, never()).onStarted(eq(PARAMS));
+
+        rangingManager.onRangingStartFailed(handle, REASON, PARAMS);
+        verify(callback, never()).onStartFailed(eq(REASON), eq(PARAMS));
+
+        RangingReport report = UwbTestUtils.getRangingReports(1);
+        rangingManager.onRangingResult(handle, report);
+        verify(callback, never()).onReportReceived(eq(report));
+
+        rangingManager.onRangingReconfigured(handle, PARAMS);
+        verify(callback, never()).onReconfigured(eq(PARAMS));
+
+        rangingManager.onRangingReconfigureFailed(handle, REASON, PARAMS);
+        verify(callback, never()).onReconfigureFailed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onRangingStopped(handle, REASON, PARAMS);
+        verify(callback, never()).onStopped(eq(REASON), eq(PARAMS));
+
+        rangingManager.onRangingStopFailed(handle, REASON, PARAMS);
+        verify(callback, never()).onStopFailed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onControleeAdded(handle, PARAMS);
+        verify(callback, never()).onControleeAdded(eq(PARAMS));
+
+        rangingManager.onControleeAddFailed(handle, REASON, PARAMS);
+        verify(callback, never()).onControleeAddFailed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onControleeRemoved(handle, PARAMS);
+        verify(callback, never()).onControleeRemoved(eq(PARAMS));
+
+        rangingManager.onControleeRemoveFailed(handle, REASON, PARAMS);
+        verify(callback, never()).onControleeRemoveFailed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onRangingPaused(handle, PARAMS);
+        verify(callback, never()).onPaused(eq(PARAMS));
+
+        rangingManager.onRangingPauseFailed(handle, REASON, PARAMS);
+        verify(callback, never()).onPauseFailed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onRangingResumed(handle, PARAMS);
+        verify(callback, never()).onResumed(eq(PARAMS));
+
+        rangingManager.onRangingResumeFailed(handle, REASON, PARAMS);
+        verify(callback, never()).onResumeFailed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onDataSent(handle, ADDRESS, PARAMS);
+        verify(callback, never()).onDataSent(eq(ADDRESS), eq(PARAMS));
+
+        rangingManager.onDataSendFailed(handle, ADDRESS, REASON, PARAMS);
+        verify(callback, never()).onDataSendFailed(eq(ADDRESS), eq(REASON), eq(PARAMS));
+
+        rangingManager.onDataReceived(handle, ADDRESS, PARAMS, DATA);
+        verify(callback, never()).onDataReceived(eq(ADDRESS), eq(PARAMS), eq(DATA));
+
+        rangingManager.onDataReceiveFailed(handle, ADDRESS, REASON, PARAMS);
+        verify(callback, never()).onDataReceiveFailed(eq(ADDRESS), eq(REASON), eq(PARAMS));
+
+        rangingManager.onServiceDiscovered(handle, PARAMS);
+        verify(callback, never()).onServiceDiscovered(eq(PARAMS));
+
+        rangingManager.onServiceConnected(handle, PARAMS);
+        verify(callback, never()).onServiceConnected(eq(PARAMS));
+
+        rangingManager.onRangingClosed(handle, REASON, PARAMS);
+        verify(callback, times(1)).onClosed(eq(REASON), eq(PARAMS));
+    }
+
+    @Test
+    public void testOnRangingClosed_MultipleSessionsRegistered() throws RemoteException {
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        // Verify that if multiple sessions are registered, only the session that is
+        // requested to close receives the associated callbacks
+        RangingSession.Callback callback1 = mock(RangingSession.Callback.class);
+        RangingSession.Callback callback2 = mock(RangingSession.Callback.class);
+
+        RangingManager rangingManager = new RangingManager(adapter);
+        ArgumentCaptor<SessionHandle> sessionHandleCaptor =
+                ArgumentCaptor.forClass(SessionHandle.class);
+
+        rangingManager.openSession(
+                ATTRIBUTION_SOURCE, PARAMS, EXECUTOR, callback1, /* chipIds= */ null);
+        verify(adapter, times(1))
+                .openRanging(
+                        eq(ATTRIBUTION_SOURCE),
+                        sessionHandleCaptor.capture(),
+                        any(),
+                        any(),
+                        eq(/* chipId= */ null));
+        SessionHandle sessionHandle1 = sessionHandleCaptor.getValue();
+
+        rangingManager.openSession(
+                ATTRIBUTION_SOURCE, PARAMS, EXECUTOR, callback2, /* chipIds= */ null);
+        verify(adapter, times(2))
+                .openRanging(
+                        eq(ATTRIBUTION_SOURCE),
+                        sessionHandleCaptor.capture(),
+                        any(),
+                        any(),
+                        eq(/* chipId= */ null));
+        SessionHandle sessionHandle2 = sessionHandleCaptor.getValue();
+
+        rangingManager.onRangingClosed(sessionHandle1, REASON, PARAMS);
+        verify(callback1, times(1)).onClosed(anyInt(), any());
+        verify(callback2, times(0)).onClosed(anyInt(), any());
+
+        rangingManager.onRangingClosed(sessionHandle2, REASON, PARAMS);
+        verify(callback1, times(1)).onClosed(anyInt(), any());
+        verify(callback2, times(1)).onClosed(anyInt(), any());
+    }
+
+    @Test
+    public void testOnRangingReport_MultipleSessionsRegistered() throws RemoteException {
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession.Callback callback1 = mock(RangingSession.Callback.class);
+        RangingSession.Callback callback2 = mock(RangingSession.Callback.class);
+
+        ArgumentCaptor<SessionHandle> sessionHandleCaptor =
+                ArgumentCaptor.forClass(SessionHandle.class);
+
+        RangingManager rangingManager = new RangingManager(adapter);
+        rangingManager.openSession(
+                ATTRIBUTION_SOURCE, PARAMS, EXECUTOR, callback1, /* chipIds= */ null);
+        verify(adapter, times(1))
+                .openRanging(
+                        eq(ATTRIBUTION_SOURCE),
+                        sessionHandleCaptor.capture(),
+                        any(),
+                        any(),
+                        eq(/* chipId= */ null));
+        SessionHandle sessionHandle1 = sessionHandleCaptor.getValue();
+
+        rangingManager.onRangingStarted(sessionHandle1, PARAMS);
+        rangingManager.openSession(
+                ATTRIBUTION_SOURCE, PARAMS, EXECUTOR, callback2, /* chipIds= */ null);
+        verify(adapter, times(2))
+                .openRanging(
+                        eq(ATTRIBUTION_SOURCE),
+                        sessionHandleCaptor.capture(),
+                        any(),
+                        any(),
+                        eq(/* chipId= */ null));
+        SessionHandle sessionHandle2 = sessionHandleCaptor.getValue();
+        rangingManager.onRangingStarted(sessionHandle2, PARAMS);
+
+        rangingManager.onRangingResult(sessionHandle1, UwbTestUtils.getRangingReports(1));
+        verify(callback1, times(1)).onReportReceived(any());
+        verify(callback2, times(0)).onReportReceived(any());
+
+        rangingManager.onRangingResult(sessionHandle2, UwbTestUtils.getRangingReports(1));
+        verify(callback1, times(1)).onReportReceived(any());
+        verify(callback2, times(1)).onReportReceived(any());
+    }
+
+    @Test
+    public void testReasons() throws RemoteException {
+        runReason(RangingChangeReason.LOCAL_API, RangingSession.Callback.REASON_LOCAL_REQUEST);
+
+        runReason(
+                RangingChangeReason.MAX_SESSIONS_REACHED,
+                RangingSession.Callback.REASON_MAX_SESSIONS_REACHED);
+
+        runReason(
+                RangingChangeReason.PROTOCOL_SPECIFIC,
+                RangingSession.Callback.REASON_PROTOCOL_SPECIFIC_ERROR);
+
+        runReason(
+                RangingChangeReason.REMOTE_REQUEST, RangingSession.Callback.REASON_REMOTE_REQUEST);
+
+        runReason(RangingChangeReason.SYSTEM_POLICY, RangingSession.Callback.REASON_SYSTEM_POLICY);
+
+        runReason(
+                RangingChangeReason.BAD_PARAMETERS, RangingSession.Callback.REASON_BAD_PARAMETERS);
+
+        runReason(RangingChangeReason.UNKNOWN, RangingSession.Callback.REASON_UNKNOWN);
+    }
+
+    private void runReason(
+            @RangingChangeReason int reasonIn, @RangingSession.Callback.Reason int reasonOut)
+            throws RemoteException {
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingManager rangingManager = new RangingManager(adapter);
+        RangingSession.Callback callback = mock(RangingSession.Callback.class);
+
+        ArgumentCaptor<SessionHandle> sessionHandleCaptor =
+                ArgumentCaptor.forClass(SessionHandle.class);
+
+        rangingManager.openSession(
+                ATTRIBUTION_SOURCE, PARAMS, EXECUTOR, callback, /* chipIds= */ null);
+        verify(adapter, times(1))
+                .openRanging(
+                        eq(ATTRIBUTION_SOURCE),
+                        sessionHandleCaptor.capture(),
+                        any(),
+                        any(),
+                        eq(/* chipId= */ null));
+        SessionHandle handle = sessionHandleCaptor.getValue();
+
+        rangingManager.onRangingOpenFailed(handle, reasonIn, PARAMS);
+        verify(callback, times(1)).onOpenFailed(eq(reasonOut), eq(PARAMS));
+
+        // Open a new session
+        rangingManager.openSession(
+                ATTRIBUTION_SOURCE, PARAMS, EXECUTOR, callback, /* chipIds= */ null);
+        verify(adapter, times(2))
+                .openRanging(
+                        eq(ATTRIBUTION_SOURCE),
+                        sessionHandleCaptor.capture(),
+                        any(),
+                        any(),
+                        eq(/* chipId= */ null));
+        handle = sessionHandleCaptor.getValue();
+        rangingManager.onRangingOpened(handle);
+
+        rangingManager.onRangingStartFailed(handle, reasonIn, PARAMS);
+        verify(callback, times(1)).onStartFailed(eq(reasonOut), eq(PARAMS));
+
+        rangingManager.onRangingReconfigureFailed(handle, reasonIn, PARAMS);
+        verify(callback, times(1)).onReconfigureFailed(eq(reasonOut), eq(PARAMS));
+
+        rangingManager.onRangingStopFailed(handle, reasonIn, PARAMS);
+        verify(callback, times(1)).onStopFailed(eq(reasonOut), eq(PARAMS));
+
+        rangingManager.onRangingClosed(handle, reasonIn, PARAMS);
+        verify(callback, times(1)).onClosed(eq(reasonOut), eq(PARAMS));
+    }
+}
diff --git a/framework/tests/src/android/uwb/UwbFrameworkInitializerTest.java b/framework/tests/src/android/uwb/UwbFrameworkInitializerTest.java
new file mode 100644
index 0000000..5883bf2
--- /dev/null
+++ b/framework/tests/src/android/uwb/UwbFrameworkInitializerTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 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.
+ */
+
+package android.uwb;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit Test of {@link android.uwb.UwbFrameworkInitializer}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class UwbFrameworkInitializerTest {
+    /**
+     * UwbFrameworkInitializer.registerServiceWrappers() should only be called by
+     * SystemServiceRegistry during boot up when Uwb is first initialized. Calling this API at any
+     * other time should throw an exception.
+     */
+    @Test
+    public void testRegisterServiceWrappers_failsWhenCalledOutsideOfSystemServiceRegistry() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> UwbFrameworkInitializer.registerServiceWrappers());
+    }
+}
diff --git a/framework/tests/src/android/uwb/UwbManagerTest.java b/framework/tests/src/android/uwb/UwbManagerTest.java
new file mode 100644
index 0000000..38fccca
--- /dev/null
+++ b/framework/tests/src/android/uwb/UwbManagerTest.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 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.
+ */
+
+package android.uwb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.uwb.UwbManager.AdapterStateCallback;
+import android.uwb.UwbManager.AdfProvisionStateCallback;
+import android.uwb.UwbManager.UwbVendorUciCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Test of {@link UwbManager}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class UwbManagerTest {
+
+    @Mock private Context mContext;
+    @Mock private IUwbAdapter mIUwbAdapter;
+    @Mock private AdapterStateCallback mAdapterStateCallback;
+    @Mock private AdapterStateCallback mAdapterStateCallback2;
+    @Mock private UwbVendorUciCallback mUwbVendorUciCallback;
+    @Mock private UwbVendorUciCallback mUwbVendorUciCallback2;
+
+    private static final Executor EXECUTOR = UwbTestUtils.getExecutor();
+    private static final String CHIP_ID = "CHIP_ID";
+    private static final PersistableBundle PARAMS = new PersistableBundle();
+    private static final PersistableBundle PARAMS2 = new PersistableBundle();
+    private static final byte[] PAYLOAD = new byte[] {0x0, 0x1};
+    private static final int GID = 9;
+    private static final int OID = 1;
+    private static final int UID = 343453;
+    private static final String PACKAGE_NAME = "com.uwb.test";
+    private static final AttributionSource ATTRIBUTION_SOURCE =
+            new AttributionSource.Builder(UID).setPackageName(PACKAGE_NAME).build();
+    private static final long TIME_NANOS = 1001;
+    private static final long TIME_NANOS2 = 1002;
+
+    private UwbManager mUwbManager;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        when(mContext.getAttributionSource()).thenReturn(ATTRIBUTION_SOURCE);
+
+        mUwbManager = new UwbManager(mContext, mIUwbAdapter);
+    }
+
+    @Test
+    public void testRegisterUnregisterCallbacks() throws Exception {
+        // Register/unregister AdapterStateCallbacks
+        mUwbManager.registerAdapterStateCallback(EXECUTOR, mAdapterStateCallback);
+        verify(mIUwbAdapter, times(1)).registerAdapterStateCallbacks(any());
+        mUwbManager.registerAdapterStateCallback(EXECUTOR, mAdapterStateCallback2);
+        verify(mIUwbAdapter, times(1)).registerAdapterStateCallbacks(any());
+        mUwbManager.unregisterAdapterStateCallback(mAdapterStateCallback);
+        verify(mIUwbAdapter, never()).unregisterAdapterStateCallbacks(any());
+        mUwbManager.unregisterAdapterStateCallback(mAdapterStateCallback2);
+        verify(mIUwbAdapter, times(1)).unregisterAdapterStateCallbacks(any());
+
+        // Register/unregister UwbVendorUciCallback
+        mUwbManager.registerUwbVendorUciCallback(EXECUTOR, mUwbVendorUciCallback);
+        verify(mIUwbAdapter, times(1)).registerVendorExtensionCallback(any());
+        mUwbManager.registerUwbVendorUciCallback(EXECUTOR, mUwbVendorUciCallback2);
+        verify(mIUwbAdapter, times(1)).registerVendorExtensionCallback(any());
+        mUwbManager.unregisterUwbVendorUciCallback(mUwbVendorUciCallback);
+        verify(mIUwbAdapter, never()).unregisterVendorExtensionCallback(any());
+        mUwbManager.unregisterUwbVendorUciCallback(mUwbVendorUciCallback2);
+        verify(mIUwbAdapter, times(1)).unregisterVendorExtensionCallback(any());
+    }
+
+    @Test
+    public void testGettersAndSetters() throws Exception {
+        // Get SpecificationInfo
+        when(mIUwbAdapter.getSpecificationInfo(/*chipId=*/ null)).thenReturn(PARAMS);
+        assertThat(mUwbManager.getSpecificationInfo()).isEqualTo(PARAMS);
+        when(mIUwbAdapter.getSpecificationInfo(CHIP_ID)).thenReturn(PARAMS2);
+        assertThat(mUwbManager.getSpecificationInfo(CHIP_ID)).isEqualTo(PARAMS2);
+        doThrow(new RemoteException()).when(mIUwbAdapter).getSpecificationInfo(/*chipId=*/ null);
+        assertThrows(RuntimeException.class, () -> mUwbManager.getSpecificationInfo());
+
+        // Get elapsedRealtimeResolutionNanos
+        when(mIUwbAdapter.getTimestampResolutionNanos(/*chipId=*/ null)).thenReturn(TIME_NANOS);
+        assertThat(mUwbManager.elapsedRealtimeResolutionNanos()).isEqualTo(TIME_NANOS);
+        when(mIUwbAdapter.getTimestampResolutionNanos(CHIP_ID)).thenReturn(TIME_NANOS2);
+        assertThat(mUwbManager.elapsedRealtimeResolutionNanos(CHIP_ID)).isEqualTo(TIME_NANOS2);
+        doThrow(new RemoteException())
+                .when(mIUwbAdapter)
+                .getTimestampResolutionNanos(/*chipId=*/ null);
+        assertThrows(RuntimeException.class, () -> mUwbManager.elapsedRealtimeResolutionNanos());
+
+        // setUwbEnabled
+        mUwbManager.setUwbEnabled(/*enabled=*/ true);
+        verify(mIUwbAdapter, times(1)).setEnabled(true);
+
+        // Get IsUwbEnabled
+        when(mIUwbAdapter.getAdapterState()).thenReturn(AdapterState.STATE_ENABLED_ACTIVE);
+        assertThat(mUwbManager.isUwbEnabled()).isTrue();
+        when(mIUwbAdapter.getAdapterState()).thenReturn(AdapterState.STATE_ENABLED_INACTIVE);
+        assertThat(mUwbManager.isUwbEnabled()).isTrue();
+        when(mIUwbAdapter.getAdapterState()).thenReturn(AdapterState.STATE_DISABLED);
+        assertThat(mUwbManager.isUwbEnabled()).isFalse();
+        doThrow(new RemoteException()).when(mIUwbAdapter).getAdapterState();
+        assertThrows(RuntimeException.class, () -> mUwbManager.isUwbEnabled());
+
+        // getChipInfos
+        when(mIUwbAdapter.getChipInfos()).thenReturn(List.of(PARAMS));
+        assertThat(mUwbManager.getChipInfos()).isEqualTo(List.of(PARAMS));
+        doThrow(new RemoteException()).when(mIUwbAdapter).getChipInfos();
+        assertThrows(RuntimeException.class, () -> mUwbManager.getChipInfos());
+
+        // getDefaultChipId
+        when(mIUwbAdapter.getDefaultChipId()).thenReturn(CHIP_ID);
+        assertThat(mUwbManager.getDefaultChipId()).isEqualTo(CHIP_ID);
+        doThrow(new RemoteException()).when(mIUwbAdapter).getDefaultChipId();
+        assertThrows(RuntimeException.class, () -> mUwbManager.getDefaultChipId());
+
+        // getAllServiceProfiles
+        when(mIUwbAdapter.getAllServiceProfiles()).thenReturn(PARAMS);
+        assertThat(mUwbManager.getAllServiceProfiles()).isEqualTo(PARAMS);
+        doThrow(new RemoteException()).when(mIUwbAdapter).getAllServiceProfiles();
+        assertThrows(RuntimeException.class, () -> mUwbManager.getAllServiceProfiles());
+
+        // getAllServiceProfiles
+        when(mIUwbAdapter.getAdfProvisioningAuthorities(PARAMS)).thenReturn(PARAMS);
+        assertThat(mUwbManager.getAdfProvisioningAuthorities(PARAMS)).isEqualTo(PARAMS);
+        doThrow(new RemoteException()).when(mIUwbAdapter).getAdfProvisioningAuthorities(PARAMS);
+        assertThrows(
+                RuntimeException.class, () -> mUwbManager.getAdfProvisioningAuthorities(PARAMS));
+
+        // getAdfCertificateInfo
+        when(mIUwbAdapter.getAdfCertificateAndInfo(PARAMS)).thenReturn(PARAMS);
+        assertThat(mUwbManager.getAdfCertificateInfo(PARAMS)).isEqualTo(PARAMS);
+        doThrow(new RemoteException()).when(mIUwbAdapter).getAdfCertificateAndInfo(PARAMS);
+        assertThrows(RuntimeException.class, () -> mUwbManager.getAdfCertificateInfo(PARAMS));
+    }
+
+    @Test
+    public void testOpenRangingSession() throws Exception {
+        RangingSession.Callback callback = mock(RangingSession.Callback.class);
+        // null chip id
+        mUwbManager.openRangingSession(PARAMS, EXECUTOR, callback);
+        verify(mIUwbAdapter, times(1))
+                .openRanging(eq(ATTRIBUTION_SOURCE), any(), any(), eq(PARAMS), eq(null));
+
+        // Chip id not on valid list
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mUwbManager.openRangingSession(PARAMS, EXECUTOR, callback, CHIP_ID));
+
+        // Chip id on valid list
+        when(mIUwbAdapter.getChipIds()).thenReturn(List.of(CHIP_ID));
+        mUwbManager.openRangingSession(PARAMS, EXECUTOR, callback, CHIP_ID);
+        verify(mIUwbAdapter, times(1))
+                .openRanging(eq(ATTRIBUTION_SOURCE), any(), any(), eq(PARAMS), eq(CHIP_ID));
+    }
+
+    @Test
+    public void testAddServiceProfile() throws Exception {
+        when(mIUwbAdapter.addServiceProfile(PARAMS)).thenReturn(PARAMS);
+        assertThat(mUwbManager.addServiceProfile(PARAMS)).isEqualTo(PARAMS);
+        doThrow(new RemoteException()).when(mIUwbAdapter).addServiceProfile(PARAMS);
+        assertThrows(RuntimeException.class, () -> mUwbManager.addServiceProfile(PARAMS));
+    }
+
+    @Test
+    public void testRemoveServiceProfile() throws Exception {
+        when(mIUwbAdapter.removeServiceProfile(PARAMS))
+                .thenReturn(UwbManager.REMOVE_SERVICE_PROFILE_SUCCESS);
+        assertThat(mUwbManager.removeServiceProfile(PARAMS))
+                .isEqualTo(UwbManager.REMOVE_SERVICE_PROFILE_SUCCESS);
+        doThrow(new RemoteException()).when(mIUwbAdapter).removeServiceProfile(PARAMS);
+        assertThrows(RuntimeException.class, () -> mUwbManager.removeServiceProfile(PARAMS));
+    }
+
+    @Test
+    public void testProvisionProfileAdfByScript() throws Exception {
+        AdfProvisionStateCallback cb = mock(AdfProvisionStateCallback.class);
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mUwbManager.provisionProfileAdfByScript(PARAMS, /*executor=*/ null, cb));
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        mUwbManager.provisionProfileAdfByScript(
+                                PARAMS, EXECUTOR, /*callback=*/ null));
+        doThrow(new RemoteException())
+                .when(mIUwbAdapter)
+                .provisionProfileAdfByScript(eq(PARAMS), any());
+        assertThrows(
+                RuntimeException.class,
+                () -> mUwbManager.provisionProfileAdfByScript(PARAMS, EXECUTOR, cb));
+    }
+
+    @Test
+    public void testRemoveProfileAdf() throws Exception {
+        when(mIUwbAdapter.removeProfileAdf(PARAMS))
+                .thenReturn(UwbManager.REMOVE_PROFILE_ADF_SUCCESS);
+        assertThat(mUwbManager.removeProfileAdf(PARAMS))
+                .isEqualTo(UwbManager.REMOVE_PROFILE_ADF_SUCCESS);
+        doThrow(new RemoteException()).when(mIUwbAdapter).removeProfileAdf(PARAMS);
+        assertThrows(RuntimeException.class, () -> mUwbManager.removeProfileAdf(PARAMS));
+    }
+
+    @Test
+    public void testSendVendorUciMessage() throws Exception {
+        when(mIUwbAdapter.sendVendorUciMessage(GID, OID, PAYLOAD))
+                .thenReturn(UwbManager.SEND_VENDOR_UCI_SUCCESS);
+        assertThat(mUwbManager.sendVendorUciMessage(GID, OID, PAYLOAD))
+                .isEqualTo(UwbManager.SEND_VENDOR_UCI_SUCCESS);
+        doThrow(new RemoteException()).when(mIUwbAdapter).sendVendorUciMessage(GID, OID, PAYLOAD);
+        assertThrows(
+                RuntimeException.class, () -> mUwbManager.sendVendorUciMessage(GID, OID, PAYLOAD));
+    }
+}
diff --git a/framework/tests/src/android/uwb/UwbTestUtils.java b/framework/tests/src/android/uwb/UwbTestUtils.java
new file mode 100644
index 0000000..75c6924
--- /dev/null
+++ b/framework/tests/src/android/uwb/UwbTestUtils.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.uwb;
+
+import android.os.SystemClock;
+
+import java.util.concurrent.Executor;
+
+public class UwbTestUtils {
+    private UwbTestUtils() {}
+
+    public static AngleMeasurement getAngleMeasurement() {
+        return new AngleMeasurement(
+                getDoubleInRange(-Math.PI, Math.PI),
+                getDoubleInRange(0, Math.PI),
+                getDoubleInRange(0, 1));
+    }
+
+    public static AngleOfArrivalMeasurement getAngleOfArrivalMeasurement() {
+        return new AngleOfArrivalMeasurement.Builder(getAngleMeasurement())
+                .setAltitude(getAngleMeasurement())
+                .build();
+    }
+
+    public static DistanceMeasurement getDistanceMeasurement() {
+        return new DistanceMeasurement.Builder()
+                .setMeters(getDoubleInRange(0, 100))
+                .setErrorMeters(getDoubleInRange(0, 10))
+                .setConfidenceLevel(getDoubleInRange(0, 1))
+                .build();
+    }
+
+    public static RangingMeasurement getRangingMeasurement() {
+        return getRangingMeasurement(getUwbAddress(false));
+    }
+
+    public static RangingMeasurement getRangingMeasurement(UwbAddress address) {
+        return new RangingMeasurement.Builder()
+                .setDistanceMeasurement(getDistanceMeasurement())
+                .setAngleOfArrivalMeasurement(getAngleOfArrivalMeasurement())
+                .setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos())
+                .setRemoteDeviceAddress(address != null ? address : getUwbAddress(false))
+                .setStatus(RangingMeasurement.RANGING_STATUS_SUCCESS)
+                .build();
+    }
+
+    public static RangingReport getRangingReports(int numMeasurements) {
+        RangingReport.Builder builder = new RangingReport.Builder();
+        for (int i = 0; i < numMeasurements; i++) {
+            builder.addMeasurement(getRangingMeasurement());
+        }
+        return builder.build();
+    }
+
+    private static double getDoubleInRange(double min, double max) {
+        return min + (max - min) * Math.random();
+    }
+
+    public static UwbAddress getUwbAddress(boolean isShortAddress) {
+        byte[] addressBytes = new byte[isShortAddress ? UwbAddress.SHORT_ADDRESS_BYTE_LENGTH :
+                UwbAddress.EXTENDED_ADDRESS_BYTE_LENGTH];
+        for (int i = 0; i < addressBytes.length; i++) {
+            addressBytes[i] = (byte) getDoubleInRange(1, 255);
+        }
+        return UwbAddress.fromBytes(addressBytes);
+    }
+
+    public static Executor getExecutor() {
+        return new Executor() {
+            @Override
+            public void execute(Runnable command) {
+                command.run();
+            }
+        };
+    }
+}
diff --git a/framework/tests/src/android/uwb/UwbVendorUciCallbackListenerTest.java b/framework/tests/src/android/uwb/UwbVendorUciCallbackListenerTest.java
new file mode 100644
index 0000000..e94f025
--- /dev/null
+++ b/framework/tests/src/android/uwb/UwbVendorUciCallbackListenerTest.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 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.
+ */
+
+package android.uwb;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.os.RemoteException;
+import android.uwb.UwbManager.UwbVendorUciCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Test of {@link UwbVendorUciCallbackListener}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class UwbVendorUciCallbackListenerTest {
+
+    @Mock private IUwbAdapter mIUwbAdapter;
+    @Mock private UwbVendorUciCallback mUwbVendorUciCallback;
+    @Mock private UwbVendorUciCallback mUwbVendorUciCallback2;
+
+    private static final Executor EXECUTOR = UwbTestUtils.getExecutor();
+    private static final byte[] PAYLOAD = new byte[] {0x0, 0x1};
+    private static final int GID = 9;
+    private static final int OID = 1;
+
+    private UwbVendorUciCallbackListener mUwbVendorUciCallbackListener;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mUwbVendorUciCallbackListener = new UwbVendorUciCallbackListener(mIUwbAdapter);
+    }
+
+    @Test
+    public void testRegisterUnregister() throws Exception {
+        // Register first callback
+        mUwbVendorUciCallbackListener.register(EXECUTOR, mUwbVendorUciCallback);
+        verify(mIUwbAdapter, times(1))
+                .registerVendorExtensionCallback(mUwbVendorUciCallbackListener);
+        // Register first callback again, no call to adapter register
+        mUwbVendorUciCallbackListener.register(EXECUTOR, mUwbVendorUciCallback);
+        verify(mIUwbAdapter, times(1))
+                .registerVendorExtensionCallback(mUwbVendorUciCallbackListener);
+        // Register second callback, no call to adapter register
+        mUwbVendorUciCallbackListener.register(EXECUTOR, mUwbVendorUciCallback2);
+        verify(mIUwbAdapter, times(1))
+                .registerVendorExtensionCallback(mUwbVendorUciCallbackListener);
+        // Unrgister first callback, no call to adapter unregister
+        mUwbVendorUciCallbackListener.unregister(mUwbVendorUciCallback);
+        verify(mIUwbAdapter, never())
+                .unregisterVendorExtensionCallback(mUwbVendorUciCallbackListener);
+        // Unrgister first callback again, no call to adapter unregister
+        mUwbVendorUciCallbackListener.unregister(mUwbVendorUciCallback);
+        verify(mIUwbAdapter, never())
+                .unregisterVendorExtensionCallback(mUwbVendorUciCallbackListener);
+        // Unregister second callback
+        mUwbVendorUciCallbackListener.unregister(mUwbVendorUciCallback2);
+        verify(mIUwbAdapter, times(1))
+                .unregisterVendorExtensionCallback(mUwbVendorUciCallbackListener);
+    }
+
+    @Test
+    public void testRegister_failedThrowsRuntimeException() throws Exception {
+        doThrow(new RemoteException())
+                .when(mIUwbAdapter)
+                .registerVendorExtensionCallback(mUwbVendorUciCallbackListener);
+        assertThrows(
+                RuntimeException.class,
+                () -> mUwbVendorUciCallbackListener.register(EXECUTOR, mUwbVendorUciCallback));
+    }
+
+    @Test
+    public void testUnregister_failedThrowsRuntimeException() throws Exception {
+        mUwbVendorUciCallbackListener.register(EXECUTOR, mUwbVendorUciCallback);
+        doThrow(new RemoteException())
+                .when(mIUwbAdapter)
+                .unregisterVendorExtensionCallback(mUwbVendorUciCallbackListener);
+
+        assertThrows(
+                RuntimeException.class,
+                () -> mUwbVendorUciCallbackListener.unregister(mUwbVendorUciCallback));
+    }
+
+    Answer mRegisterSuccessResponse =
+            new Answer() {
+                public Object answer(InvocationOnMock invocation) {
+                    Object[] args = invocation.getArguments();
+                    IUwbVendorUciCallback cb = (IUwbVendorUciCallback) args[0];
+                    try {
+                        cb.onVendorResponseReceived(GID, OID, PAYLOAD);
+                    } catch (RemoteException e) {
+                        // Nothing to do
+                    }
+                    return new Object();
+                }
+            };
+
+    @Test
+    public void testOnVendorResponseReceived_succeed() throws Exception {
+        doAnswer(mRegisterSuccessResponse)
+                .when(mIUwbAdapter)
+                .registerVendorExtensionCallback(any());
+        // Register first callback
+        mUwbVendorUciCallbackListener.register(EXECUTOR, mUwbVendorUciCallback);
+        verify(mUwbVendorUciCallback, times(1)).onVendorUciResponse(GID, OID, PAYLOAD);
+        verifyZeroInteractions(mUwbVendorUciCallback2);
+        // Register second callback
+        mUwbVendorUciCallbackListener.register(EXECUTOR, mUwbVendorUciCallback2);
+        verify(mUwbVendorUciCallback, times(1)).onVendorUciResponse(GID, OID, PAYLOAD);
+        verify(mUwbVendorUciCallback2, never()).onVendorUciResponse(GID, OID, PAYLOAD);
+
+        mUwbVendorUciCallbackListener.onVendorResponseReceived(GID, OID, PAYLOAD);
+        verify(mUwbVendorUciCallback, times(2)).onVendorUciResponse(GID, OID, PAYLOAD);
+        verify(mUwbVendorUciCallback2, times(1)).onVendorUciResponse(GID, OID, PAYLOAD);
+
+        mUwbVendorUciCallbackListener.unregister(mUwbVendorUciCallback);
+        mUwbVendorUciCallbackListener.onVendorResponseReceived(GID, OID, PAYLOAD);
+        verifyNoMoreInteractions(mUwbVendorUciCallback);
+        verify(mUwbVendorUciCallback2, times(2)).onVendorUciResponse(GID, OID, PAYLOAD);
+    }
+
+    @Test
+    public void testOnVendorResponseReceived_failedThrowsRuntimeException() throws Exception {
+        doAnswer(mRegisterSuccessResponse)
+                .when(mIUwbAdapter)
+                .registerVendorExtensionCallback(any());
+        doThrow(new RuntimeException())
+                .when(mUwbVendorUciCallback)
+                .onVendorUciResponse(anyInt(), anyInt(), any());
+        assertThrows(
+                RuntimeException.class,
+                () -> mUwbVendorUciCallbackListener.register(EXECUTOR, mUwbVendorUciCallback));
+    }
+
+    Answer mRegisterSuccessNotification =
+            new Answer() {
+                public Object answer(InvocationOnMock invocation) {
+                    Object[] args = invocation.getArguments();
+                    IUwbVendorUciCallback cb = (IUwbVendorUciCallback) args[0];
+                    try {
+                        cb.onVendorNotificationReceived(GID, OID, PAYLOAD);
+                    } catch (RemoteException e) {
+                        // Nothing to do
+                    }
+                    return new Object();
+                }
+            };
+
+    @Test
+    public void testOnVendorNotificationReceived_succeed() throws Exception {
+        doAnswer(mRegisterSuccessNotification)
+                .when(mIUwbAdapter)
+                .registerVendorExtensionCallback(any());
+        // Register first callback
+        mUwbVendorUciCallbackListener.register(EXECUTOR, mUwbVendorUciCallback);
+        verify(mUwbVendorUciCallback, times(1)).onVendorUciNotification(GID, OID, PAYLOAD);
+        verifyZeroInteractions(mUwbVendorUciCallback2);
+        // Register second callback
+        mUwbVendorUciCallbackListener.register(EXECUTOR, mUwbVendorUciCallback2);
+        verify(mUwbVendorUciCallback, times(1)).onVendorUciNotification(GID, OID, PAYLOAD);
+        verify(mUwbVendorUciCallback2, never()).onVendorUciNotification(GID, OID, PAYLOAD);
+
+        mUwbVendorUciCallbackListener.onVendorNotificationReceived(GID, OID, PAYLOAD);
+        verify(mUwbVendorUciCallback, times(2)).onVendorUciNotification(GID, OID, PAYLOAD);
+        verify(mUwbVendorUciCallback2, times(1)).onVendorUciNotification(GID, OID, PAYLOAD);
+
+        mUwbVendorUciCallbackListener.unregister(mUwbVendorUciCallback);
+        mUwbVendorUciCallbackListener.onVendorNotificationReceived(GID, OID, PAYLOAD);
+        verifyNoMoreInteractions(mUwbVendorUciCallback);
+        verify(mUwbVendorUciCallback2, times(2)).onVendorUciNotification(GID, OID, PAYLOAD);
+    }
+
+    @Test
+    public void testOnVendorNotificationReceived_failedThrowsRuntimeException() throws Exception {
+        doAnswer(mRegisterSuccessNotification)
+                .when(mIUwbAdapter)
+                .registerVendorExtensionCallback(any());
+        doThrow(new RuntimeException())
+                .when(mUwbVendorUciCallback)
+                .onVendorUciNotification(anyInt(), anyInt(), any());
+        assertThrows(
+                RuntimeException.class,
+                () -> mUwbVendorUciCallbackListener.register(EXECUTOR, mUwbVendorUciCallback));
+    }
+}
diff --git a/framework/tests/test-jarjar-rules.txt b/framework/tests/test-jarjar-rules.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/framework/tests/test-jarjar-rules.txt
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..617d425
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,5 @@
+# Android Format Style
+
+edition = "2018"
+use_small_heuristics = "Max"
+newline_style = "Unix"
diff --git a/service/Android.bp b/service/Android.bp
new file mode 100644
index 0000000..b998058
--- /dev/null
+++ b/service/Android.bp
@@ -0,0 +1,123 @@
+//
+// 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.
+//
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+    name: "service-uwb-common-defaults",
+    defaults: ["uwb-module-sdk-version-defaults"],
+    errorprone: {
+        javacflags: ["-Xep:CheckReturnValue:ERROR"],
+    },
+    product_variables: {
+        pdk: {
+            enabled: false,
+        },
+    },
+}
+
+filegroup {
+    name: "service-uwb-srcs",
+    srcs: [
+        "java/**/*.java",
+        ":statslog-uwb-java-gen",
+        ":uwb_config"
+    ],
+}
+
+// pre-jarjar version of service-uwb that builds against pre-jarjar version of framework-uwb
+java_library {
+    name: "service-uwb-pre-jarjar",
+    installable: false,
+    defaults: ["service-uwb-common-defaults"],
+    srcs: [ ":service-uwb-srcs" ],
+    // java_api_finder must accompany `srcs`
+    plugins: ["java_api_finder"],
+    required: ["libuwb_uci_jni_rust"],
+    sdk_version: "system_server_Tiramisu",
+
+    lint: {
+        strict_updatability_linting: true,
+    },
+    libs: [
+        "framework-annotations-lib",
+        "framework-uwb-pre-jarjar",
+        "ServiceUwbResources",
+        "framework-statsd.stubs.module_lib",
+        "framework-wifi.stubs.module_lib",
+    ],
+
+    static_libs: [
+        "androidx.annotation_annotation",
+        "android.hardware.uwb.fira_android-V1-java",
+        "com.uwb.support.ccc",
+        "com.uwb.support.fira",
+        "com.uwb.support.generic",
+        "com.uwb.support.multichip",
+        "com.uwb.support.profile",
+        "modules-utils-shell-command-handler",
+        "modules-utils-handlerexecutor",
+        "modules-utils-preconditions",
+        "guava",
+    ],
+
+    apex_available: [
+        "com.android.uwb",
+    ],
+}
+
+// service-uwb static library
+// ============================================================
+java_library {
+    name: "service-uwb",
+    defaults: ["service-uwb-common-defaults"],
+    installable: true,
+    static_libs: ["service-uwb-pre-jarjar"],
+
+    // need to include `libs` so that Soong doesn't complain about missing classes after jarjaring
+    libs: [
+        "framework-uwb.impl",
+    ],
+
+    sdk_version: "system_server_Tiramisu",
+
+    jarjar_rules: ":uwb-jarjar-rules",
+    optimize: {
+        enabled: true,
+        shrink: true,
+        proguard_flags_files: ["proguard.flags"],
+    },
+
+    visibility: [
+        "//packages/modules/Uwb/apex",
+        "//packages/modules/Uwb/service/tests/uwbtests/apex",
+    ],
+    apex_available: [
+        "com.android.uwb",
+    ],
+}
+
+// Statsd auto-generated code
+// ============================================================
+genrule {
+    name: "statslog-uwb-java-gen",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --java $(out) --module uwb " +
+         " --javaPackage com.android.server.uwb.proto --javaClass UwbStatsLog" +
+         " --minApiLevel 33",
+    out: ["com/android/server/uwb/proto/UwbStatsLog.java"],
+}
diff --git a/service/OWNERS b/service/OWNERS
new file mode 100644
index 0000000..c31a2f1
--- /dev/null
+++ b/service/OWNERS
@@ -0,0 +1 @@
+include platform/frameworks/base:/core/java/android/uwb/OWNERS
diff --git a/service/ServiceUwbResources/Android.bp b/service/ServiceUwbResources/Android.bp
new file mode 100644
index 0000000..af67c9e
--- /dev/null
+++ b/service/ServiceUwbResources/Android.bp
@@ -0,0 +1,40 @@
+//
+// 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.
+//
+
+// APK to hold all the uwb overlayable resources.
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "ServiceUwbResources",
+    defaults: ["service-uwb-common-defaults"],
+    resource_dirs: [
+        "res",
+    ],
+    privileged: true,
+    sdk_version: "system_Tiramisu",
+    export_package_resources: true,
+    apex_available: [
+        "com.android.uwb",
+    ],
+    certificate: ":com.android.uwb.resources.certificate",
+}
+
+android_app_certificate {
+    name: "com.android.uwb.resources.certificate",
+    certificate: "resources-certs/com.android.uwb.resources"
+}
diff --git a/service/ServiceUwbResources/AndroidManifest.xml b/service/ServiceUwbResources/AndroidManifest.xml
new file mode 100644
index 0000000..c91c950
--- /dev/null
+++ b/service/ServiceUwbResources/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<!-- Manifest for uwb resources APK -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.uwb.resources"
+          coreApp="true"
+          android:versionCode="1"
+          android:versionName="T-initial">
+    <application
+        android:label="@string/uwbResourcesAppLabel"
+        android:defaultToDeviceProtectedStorage="true"
+        android:directBootAware="true"
+        android:usesCleartextTraffic="true">
+        <!-- This is only used to identify this app by resolving the action.
+             The activity is never actually triggered. -->
+        <activity
+            android:name="android.app.Activity"
+            android:exported="true"
+            android:enabled="true">
+            <intent-filter>
+                <action
+                    android:name="com.android.server.uwb.intent.action.SERVICE_UWB_RESOURCES_APK"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/service/ServiceUwbResources/res/values/config.xml b/service/ServiceUwbResources/res/values/config.xml
new file mode 100644
index 0000000..51e0bdb
--- /dev/null
+++ b/service/ServiceUwbResources/res/values/config.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- These resources are around just to allow their values to be customized
+     for different hardware and product builds.  Do not translate.
+
+     NOTE: The naming convention is "config_camelCaseValue". Some legacy
+     entries do not follow the convention, but all new entries should. -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- boolean indicating whether or not the system supports multiple UWB chips -->
+    <bool name="config_isMultichip">false</bool>
+
+    <!-- Filepath of multichip configuration file. This is an xml file that follows the format
+    specified in multichip-parser/uwbConfig.xsd. If config_isMultichip is false, this value will be
+    ignored. -->
+    <string translatable="false" name="config_multichipConfigPath"></string>
+</resources>
diff --git a/service/ServiceUwbResources/res/values/overlayable.xml b/service/ServiceUwbResources/res/values/overlayable.xml
new file mode 100644
index 0000000..0ff3d39
--- /dev/null
+++ b/service/ServiceUwbResources/res/values/overlayable.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- These values can be used to control uwb stack behavior/features on individual devices.
+     These can be overridden by OEM's by using an RRO overlay app.
+     See device/google/coral/rro_overlays/UwbOverlay/ for a sample overlay app. -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <overlayable name="UwbCustomization">
+        <!-- START VENDOR CUSTOMIZATION -->
+        <policy type="product|system|vendor">
+
+          <!-- Params from config.xml that can be overlaid -->
+            <item name="config_isMultichip" type="bool" />
+            <item name="config_multichipConfigPath" type="string" />
+          <!-- Params from config.xml that can be overlaid -->
+
+          <!-- Params from strings.xml that can be overlaid -->
+          <!-- Params from strings.xml that can be overlaid -->
+
+          <!-- Params from styles.xml that can be overlaid -->
+          <!-- Params from styles.xml that can be overlaid -->
+
+          <!-- Params from drawable/ that can be overlaid -->
+          <!-- Params from drawable/ that can be overlaid -->
+
+          <!-- Params from layout/ that can be overlaid -->
+          <!-- Params from layout/ that can be overlaid -->
+
+        </policy>
+        <!-- END VENDOR CUSTOMIZATION -->
+    </overlayable>
+</resources>
diff --git a/service/ServiceUwbResources/res/values/strings.xml b/service/ServiceUwbResources/res/values/strings.xml
new file mode 100644
index 0000000..7cd4ae8
--- /dev/null
+++ b/service/ServiceUwbResources/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Official label of the uwb stack -->
+    <string name="uwbResourcesAppLabel" product="default">System UWB Resources</string>
+</resources>
diff --git a/service/ServiceUwbResources/res/values/styles.xml b/service/ServiceUwbResources/res/values/styles.xml
new file mode 100644
index 0000000..128978d
--- /dev/null
+++ b/service/ServiceUwbResources/res/values/styles.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+</resources>
diff --git a/service/ServiceUwbResources/resources-certs/com.android.uwb.resources.pk8 b/service/ServiceUwbResources/resources-certs/com.android.uwb.resources.pk8
new file mode 100644
index 0000000..5aad23e
--- /dev/null
+++ b/service/ServiceUwbResources/resources-certs/com.android.uwb.resources.pk8
Binary files differ
diff --git a/service/ServiceUwbResources/resources-certs/com.android.uwb.resources.x509.pem b/service/ServiceUwbResources/resources-certs/com.android.uwb.resources.x509.pem
new file mode 100644
index 0000000..bae3d93
--- /dev/null
+++ b/service/ServiceUwbResources/resources-certs/com.android.uwb.resources.x509.pem
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIGJTCCBA2gAwIBAgIULHS/P4Q9zxZinQ/Q1MZXEnn5I0AwDQYJKoZIhvcNAQEL
+BQAwgaAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
+DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy
+b2lkMRwwGgYDVQQDDBNTZXJ2aWNlVXdiUmVzb3VyY2VzMSIwIAYJKoZIhvcNAQkB
+FhNhbmRyb2lkQGFuZHJvaWQuY29tMCAXDTIxMTEwMjE3MjY1N1oYDzQ3NTkwOTI5
+MTcyNjU3WjCBoDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAU
+BgNVBAcMDU1vdW50YWluIFZpZXcxEDAOBgNVBAoMB0FuZHJvaWQxEDAOBgNVBAsM
+B0FuZHJvaWQxHDAaBgNVBAMME1NlcnZpY2VVd2JSZXNvdXJjZXMxIjAgBgkqhkiG
+9w0BCQEWE2FuZHJvaWRAYW5kcm9pZC5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQDFHXF+IxhW7Fi90egUMUjlNLx4bPqsdyVZByOGd0jki954qkfG
+ANuJ1cMrDa5eGAl373nxYsg1Kr8suShzaqC5UE3AITdUriO0Is0gp954hrar+aDY
+RPpyI4wrnCkIc9lCf8oDof9fxosbCJp+av5BGBXWKNjac2+RUEtwxCK7Z5KrvDZw
+RZ5yta1uE/BC1ptOE0pfvKgQQGc6mbEQXWOUOYf/AiXz99Vr5/RbQEnrZTJ1TMpl
+zBufKiPxbCh/y9cBLd8Sz362kx4zp/dfOsaPsOWaLB2q0EItI3LWM02f5gJrfbq+
+o1tEP9EBtQK8QYoS7ealGSSRg6ae7bMQ3ibMdggfy5sGr0gesO69yu69kGvkq5QQ
+bjNuIazG/hV9L8rIvEKkjYbRWZFGjhCrMIG/8cnf58h4cJitAsfGS2DbrpxNIh1r
+Y0jl9CZT6dTGez3a/OBXDxGBtDKVBUHQtlm0qGWoSgUNSkC0MpuHG3M8C2sBxosS
+UMP6UAmIKyCS1swvhCPnJBfo8qloSbbRBEGs2pWQBj0Yz0zKBkZdLS4tNcijjIm0
+gKlWC7lm3f3tRtTIdxWRPMJn1k6p7Mu8YxalRkpH7ONkDJo5+5MvBJN27yL2hKxd
+fJehquaiHiD9hORN71kt6SlwPtGWSSBQuKuh4P7QusM8p8umitFgpaqa1wIDAQAB
+o1MwUTAdBgNVHQ4EFgQUNClLTwT5KpWh9mueOm/gjdBw0xwwHwYDVR0jBBgwFoAU
+NClLTwT5KpWh9mueOm/gjdBw0xwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
+AQsFAAOCAgEAmQPA7yZFuq/fyw7Y5uNA8JphkWyXUv4/HCsCiYfLWaU9Zfyp/Lp6
+S1nMTsr3Jxd8x2BOqWAYViuH5eC07O+uwVOg3/7KafEdDbgyDh2WaEocuCcxraVs
+TDzvw+nq2T8VuYIk814Wtr16N0O5j69OPieQ+F65b1WyDmDUhs3RgpWDdRC1kbCt
+CJ/F42Z8ijP+QLCPj9EaB0oGa3EG1+VQUNWgHeHaFhNrIr4n/Wh9vhZsdCme0uhR
+dg83h1ZE01UsC/j9oCAzZ/Vt6O4O4pftPRqCfaucEHl/al7cQP992HSzstiIXCgA
+nxLmZAXpDKU2ytZD02wEARfmzCLKO4Ou98V0+88EGckbDwzI7sOX8fn+80ToFXKV
+diM60fhPnuJfwFEiAz1LGG/xMVutNLq+6wFE//sUFd+lkqB4dHomjy9IqPuSUCy/
+AAc695sltAWY6VSce+lTXAlMNhVCDjugHFnmNPT1gqwDoK4KBH0Rg0K0fjqidq9l
+XtmqHsB94jMwzeyY4l0zXobSgzZ5IBW5ccscs97rmrbVtANeU94oH9My1xhaq98e
+oHPyRtHaBVtpDVaWiqDuJYfnb5rAC/EglETKOsynOon3iGQdh4cCHIVPJ3cIt+j7
+5PFFLOSBD/Qq456eQ4awSrUKJwct9CliYuEuUsmA9qNj88DalB1L4iE=
+-----END CERTIFICATE-----
diff --git a/service/ServiceUwbResources/resources-certs/key.pem b/service/ServiceUwbResources/resources-certs/key.pem
new file mode 100644
index 0000000..7bad97f
--- /dev/null
+++ b/service/ServiceUwbResources/resources-certs/key.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDFHXF+IxhW7Fi9
+0egUMUjlNLx4bPqsdyVZByOGd0jki954qkfGANuJ1cMrDa5eGAl373nxYsg1Kr8s
+uShzaqC5UE3AITdUriO0Is0gp954hrar+aDYRPpyI4wrnCkIc9lCf8oDof9fxosb
+CJp+av5BGBXWKNjac2+RUEtwxCK7Z5KrvDZwRZ5yta1uE/BC1ptOE0pfvKgQQGc6
+mbEQXWOUOYf/AiXz99Vr5/RbQEnrZTJ1TMplzBufKiPxbCh/y9cBLd8Sz362kx4z
+p/dfOsaPsOWaLB2q0EItI3LWM02f5gJrfbq+o1tEP9EBtQK8QYoS7ealGSSRg6ae
+7bMQ3ibMdggfy5sGr0gesO69yu69kGvkq5QQbjNuIazG/hV9L8rIvEKkjYbRWZFG
+jhCrMIG/8cnf58h4cJitAsfGS2DbrpxNIh1rY0jl9CZT6dTGez3a/OBXDxGBtDKV
+BUHQtlm0qGWoSgUNSkC0MpuHG3M8C2sBxosSUMP6UAmIKyCS1swvhCPnJBfo8qlo
+SbbRBEGs2pWQBj0Yz0zKBkZdLS4tNcijjIm0gKlWC7lm3f3tRtTIdxWRPMJn1k6p
+7Mu8YxalRkpH7ONkDJo5+5MvBJN27yL2hKxdfJehquaiHiD9hORN71kt6SlwPtGW
+SSBQuKuh4P7QusM8p8umitFgpaqa1wIDAQABAoICAG9p9ANa7N/82S/5nFcFgHFl
+fH4JAytGcQrAOTlA5SehU08a2FS3mV9wPs9v/TXcGkX7Sw7ASe+bSNGLmqyaAVyd
+YkUNwUuQ3kdsQEuL9vhjFI9iGqMDYhfvtcPUkXDgolWvC01AXKsS+v99vm1kJnA+
+n+Eb126qPs6A9xM3GXaZ1VJSoOaWwzeNLwkAJhZxXPkleC1i4g/Fh1OdGXLphmZ6
+mj7uROuAEtbaFckaLm3qFjos/F3Ln3U4iXZlLwMFAXL+LY9hTvr9bt29u9nMy/zd
+/PlGpEIvUqhW2Arwlpihlo6RU2N7zBNoisePO4uS8+s9ItgAvSWupxg/vK31EzIR
+64RaxCINqIi/tjwVmNd4gJS9FIXRxl5GwpXXXmwp+CvnJQ8j42/nzNyD+c4xqpEF
+Wsv99j1Lb9sOM5CbBg86nzhFu8LqxiACthP19BxYoJPTrA3LsVgeGG4QwxF4tEbz
+hv0yiFPbc8/cyfE0HKrbpQxxHsXnSFwVFrQLD1WnAQ/TKqWQAEbLoVwHd1COO2SO
+Sry192PjeOvQRbsBjDD4GbTflaW8XvzMSxwLXrnOohdc8DEMAUNeMQIxlmg2Q2Dy
+oD6dhPxdQlBLW5QEd5NFq7PlrENJ85Nwn3+PlH4CaMHp4Y00JcOjZfQLyc0m298h
+LOYDhY5/tNmYhU7ULaGJAoIBAQDrxjB6+V+lU4opMqzRJuETcMuKTiTRi/Tt7a6l
+/yHb5VJ+/GbCD1ZbGouiTZaoXwMc07bVVFNfl2w/V4a+kUmwLjXhPAByyWu/RQFk
+EReWkPb5mKQnjq91c9yqwB9NbVBBf91vkhAY/f4FhiGNPbLxyFaFTb8HVImokl/H
+jR4RV+x52aSz1pUoTLqHCskMbnt3SYx4v981tX4mXV5mIlQGKh7JFRSCFUrD/Rj0
+ygo38d5iUKZPj6gMPiivnJpDt1+JnYiFO8RAbGkrrggKxjrEjSrNlovY0LGJNPP+
+XLiO3Nrz8Sud0szwIP3o2VLo6GzAbpP1+3Po+MgBNsOpdjKNAoIBAQDWBkOq1DiA
+LQaHz45xq9naT+EXiKxeS8P/bpfJv7YV8BsDccAX0HBV9Q1Vm2IFMl9JYjWzL32B
+3ZUeCTHFx5KY6bTkH1Pvsi1YRSxRpFjeTBDltY7YVzU9ksJE81yWUqCY3R8CsAmA
+kn0dnIhBM7YWfdqkQdvU78laMHxynk7nennXikiq6nkBmYSEeL0Fys+9PXdoqtEv
+L3VU6+jhxPIuQMsX9VMACBAYXLCb4qCkXNfbXtZKDsS9xHRsDneHkggLIf08/V5O
+7aMpOeQVwlFP3NX0WyYxZyEykiT/apxAznd2eqPfd3PJeLJnfkr0B9xAu8EBbu5F
+C2e5FRlwotvzAoIBAGwdQXXijD1fhWdG2YA+987WVj9hffio2PORnhh4WapgCeg5
+DVXHeq3kCkuukHs8tkytuJUySdj3sqeJFzyjmsqzJfnWbc41FrdqiSy9ubdNWjMy
+D3QkNckCDBowZyo2Cis+2ueibsdHEQivbQs7U6cTWrld4U8XMNif5lO3HiaNzt5B
+MwlUSKlmJdJu26pbrzoP+94S/eO/Cc3F2teyvhzli6BhjjnoUZR1ps/5JZ5pxrQG
+j3zEPyb+CeIdSY/rsl+EYWnW8jMog0GIWB+4rpIauZn0gsQ1TnPAWHI3SloYZD6g
+RIPmehtSxZvUq/QpQFUtX6PYXlpiWjRUTHyUurECggEBAJyN+oOEN0wzI1rGzZiC
+r5nM4oc3d3aGj3lSKX/vVz9W4juzwmLpGrMVzMo5HgtVHHRufX90FqefMUvGR/03
+jhmCospXzCtCt36hItkZkdQR6i5Nj47aw9wldSvApJJlIIqQ/PUXVewRu6mkbdrb
++68aIowSjL6HJE9vtiiVenxCj8vFoIA9gYRVCqVoOER7ZVg1FRqgEOImIfqbkj9L
+tCd3R9hfoHYeb7+SVbHBpeZ074TNK40CnpF9mffM4Uxu2qliFH6/i3PKypYGfbwY
+5ye3D15uKlLq8FKwqpWXI3MYVDR7Y1G8bBsMyduAe01kTo2fiYAF6A7jV9z//Rry
+VlcCggEAVvBATHw5cfgn2MZSACRBXggylmkgP/sj2vv0rrVijXiYOMW81/U2yuzb
+M3M3rbPzWhv4pkVGNDcNKGNqlGjaD/aqeVTZNR/tIx9t2S9XvSQghXLnKrbHISYD
+h5xZKWApSp41XxGI2Lk2S+cC+f8OOx5F0yvGl6BFEchnRM8TVlb0Lrta+pX/HllT
+ubZ2Grfh6VGg5wMj/6/wOM2Ea9IDqaweEcZAY328tUKOKzi4/pP/QZ7lDANakhBl
+ZqCb3tmOPOyibar5pfikkAb0QdlvRLNpUaFGFTtowSWLdmfMwDjBO0BOwt5Jl50X
+Nr4s51dMh3FtU65JDDcDEmYsSaO/+A==
+-----END PRIVATE KEY-----
diff --git a/service/frameworks_uwb.iml b/service/frameworks_uwb.iml
new file mode 100644
index 0000000..0189dfe
--- /dev/null
+++ b/service/frameworks_uwb.iml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+    <component name="NewModuleRootManager" inherit-compiler-output="true">
+        <exclude-output />
+        <content url="file:///usr/local/google/home/rpius/Work/Android/internal/master/frameworks/base/services/uwb">
+            <sourceFolder url="file:///usr/local/google/home/rpius/Work/Android/internal/master/frameworks/base/services/uwb/java" isTestSource="false" />
+        </content>
+        <orderEntry type="sourceFolder" forTests="false" />
+        <orderEntry type="module" module-name="framework_srcjars" />
+        <orderEntry type="module" module-name="base" />
+        <orderEntry type="module" module-name="packages_Uwb" />
+        <orderEntry type="module" module-name="dependencies" />
+        <orderEntry type="inheritedJdk" />
+    </component>
+</module>
diff --git a/service/java/com/android/server/uwb/DeviceConfigFacade.java b/service/java/com/android/server/uwb/DeviceConfigFacade.java
new file mode 100644
index 0000000..cd84196
--- /dev/null
+++ b/service/java/com/android/server/uwb/DeviceConfigFacade.java
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import android.os.Handler;
+import android.provider.DeviceConfig;
+
+/**
+ * This class allows getting all configurable flags from DeviceConfig.
+ */
+public class DeviceConfigFacade {
+    public static final int DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS = 5_000;
+    public static final int DEFAULT_BUG_REPORT_MIN_INTERVAL_MS = 24 * 3_600_000;
+
+    private final UwbInjector mUwbInjector;
+
+    // Cached values of fields updated via updateDeviceConfigFlags()
+    private int mRangingResultLogIntervalMs;
+    private boolean mDeviceErrorBugreportEnabled;
+    private int mBugReportMinIntervalMs;
+
+    public DeviceConfigFacade(Handler handler, UwbInjector uwbInjector) {
+        mUwbInjector = uwbInjector;
+
+        updateDeviceConfigFlags();
+        DeviceConfig.addOnPropertiesChangedListener(
+                DeviceConfig.NAMESPACE_UWB,
+                command -> handler.post(command),
+                properties -> {
+                    updateDeviceConfigFlags();
+                });
+    }
+
+    private void updateDeviceConfigFlags() {
+        mRangingResultLogIntervalMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_UWB,
+                "ranging_result_log_interval_ms", DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS);
+        mDeviceErrorBugreportEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_UWB,
+                "device_error_bugreport_enabled", false);
+        mBugReportMinIntervalMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_UWB,
+                "bug_report_min_interval_ms", DEFAULT_BUG_REPORT_MIN_INTERVAL_MS);
+    }
+
+    /**
+     * Gets ranging result logging interval in ms
+     */
+    public int getRangingResultLogIntervalMs() {
+        return mRangingResultLogIntervalMs;
+    }
+
+    /**
+     * Gets the feature flag for reporting device error
+     */
+    public boolean isDeviceErrorBugreportEnabled() {
+        return mDeviceErrorBugreportEnabled;
+    }
+
+    /**
+     * Gets minimum wait time between two bug report captures
+     */
+    public int getBugReportMinIntervalMs() {
+        return mBugReportMinIntervalMs;
+    }
+}
diff --git a/service/java/com/android/server/uwb/SystemBuildProperties.java b/service/java/com/android/server/uwb/SystemBuildProperties.java
new file mode 100644
index 0000000..4090978
--- /dev/null
+++ b/service/java/com/android/server/uwb/SystemBuildProperties.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import android.os.Build;
+
+class SystemBuildProperties {
+    /** @return if it is an eng build. */
+    public boolean isEngBuild() {
+        return Build.TYPE.equals("eng");
+    }
+
+    /** @return if it is an userdebug build. */
+    public boolean isUserdebugBuild() {
+        return Build.TYPE.equals("userdebug");
+    }
+
+    /** @return if it is a normal user build. */
+    public boolean isUserBuild() {
+        return Build.TYPE.equals("user");
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbConfigurationManager.java b/service/java/com/android/server/uwb/UwbConfigurationManager.java
new file mode 100644
index 0000000..a83eef5
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbConfigurationManager.java
@@ -0,0 +1,135 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb;
+
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.server.uwb.data.UwbConfigStatusData;
+import com.android.server.uwb.data.UwbTlvData;
+import com.android.server.uwb.data.UwbUciConstants;
+import com.android.server.uwb.jni.NativeUwbManager;
+import com.android.server.uwb.params.TlvBuffer;
+import com.android.server.uwb.params.TlvDecoder;
+import com.android.server.uwb.params.TlvDecoderBuffer;
+import com.android.server.uwb.params.TlvEncoder;
+
+import com.google.uwb.support.base.Params;
+
+public class UwbConfigurationManager {
+    private static final String TAG = "UwbConfManager";
+
+    NativeUwbManager mNativeUwbManager;
+
+    public UwbConfigurationManager(NativeUwbManager nativeUwbManager) {
+        mNativeUwbManager = nativeUwbManager;
+    }
+
+    public int setAppConfigurations(int sessionId, Params params) {
+        int status = UwbUciConstants.STATUS_CODE_FAILED;
+        TlvBuffer tlvBuffer = null;
+
+        Log.d(TAG, "setAppConfigurations for protocol: " + params.getProtocolName());
+        TlvEncoder encoder = TlvEncoder.getEncoder(params.getProtocolName());
+        if (encoder == null) {
+            Log.d(TAG, "unsupported encoder protocol type");
+            return status;
+        }
+
+        tlvBuffer = encoder.getTlvBuffer(params);
+
+        if (tlvBuffer.getNoOfParams() != 0) {
+            byte[] tlvByteArray = tlvBuffer.getByteArray();
+            UwbConfigStatusData appConfig = mNativeUwbManager.setAppConfigurations(sessionId,
+                    tlvBuffer.getNoOfParams(),
+                    tlvByteArray.length, tlvByteArray);
+            Log.i(TAG, "setAppConfigurations respData: " + appConfig.toString());
+            if (appConfig != null) {
+                status = appConfig.getStatus();
+            } else {
+                Log.e(TAG, "appConfigList is null or size of appConfigList is zero");
+                status = UwbUciConstants.STATUS_CODE_FAILED;
+            }
+        } else {
+            // Number of reconfig params FiraRangingReconfigureParams can be null
+            status = UwbUciConstants.STATUS_CODE_OK;
+        }
+        return status;
+    }
+
+    /**
+     * Retrieve app configurations from UWBS.
+     */
+    public <T extends Params> Pair<Integer, T> getAppConfigurations(int sessionId,
+            String protocolName, byte[] appConfigIds, Class<T> paramType) {
+
+        Log.d(TAG, "getAppConfigurations for protocol: " + protocolName);
+        UwbTlvData getAppConfig = mNativeUwbManager.getAppConfigurations(sessionId,
+                    appConfigIds.length, appConfigIds.length, appConfigIds);
+        Log.i(TAG, "getAppConfigurations respData: "
+                + getAppConfig != null ? getAppConfig.toString() : "null");
+        return decodeTLV(protocolName, getAppConfig, paramType);
+    }
+
+    /**
+     * Retrieve capability information from UWBS.
+     */
+    public <T extends Params> Pair<Integer, T> getCapsInfo(String protocolName,
+            Class<T> paramType) {
+
+        Log.d(TAG, "getCapsInfo for protocol: " + protocolName);
+        UwbTlvData capsInfo = mNativeUwbManager.getCapsInfo();
+        Log.i(TAG, "getCapsInfo respData: " + capsInfo != null ? capsInfo.toString() : "null");
+        return decodeTLV(protocolName, capsInfo, paramType);
+    }
+
+    /**
+     * Common decode TLV function based on protocol
+     */
+    public <T extends Params> Pair<Integer, T> decodeTLV(String protocolName,
+            UwbTlvData tlvData, Class<T> paramType) {
+        int status;
+        if (tlvData != null) {
+            status = tlvData.getStatus();
+        } else {
+            Log.e(TAG, "TlvData is null or size of TlvData is zero");
+            return Pair.create(UwbUciConstants.STATUS_CODE_FAILED, null);
+        }
+        TlvDecoder decoder = TlvDecoder.getDecoder(protocolName);
+        if (decoder == null) {
+            Log.d(TAG, "unsupported decoder protocol type");
+            return Pair.create(status, null);
+        }
+
+        int numOfTlvs = tlvData.getLength();
+        TlvDecoderBuffer tlvs = new TlvDecoderBuffer(tlvData.getTlv(), numOfTlvs);
+        if (!tlvs.parse()) {
+            Log.e(TAG, "Failed to parse tlvs");
+            return Pair.create(UwbUciConstants.STATUS_CODE_FAILED, null);
+        }
+        T params = null;
+        try {
+            params = decoder.getParams(tlvs, paramType);
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "Failed to decode", e);
+        }
+        if (params == null) {
+            Log.d(TAG, "Failed to get params from tlvs");
+            return Pair.create(UwbUciConstants.STATUS_CODE_FAILED, null);
+        }
+        return Pair.create(UwbUciConstants.STATUS_CODE_OK, params);
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbContext.java b/service/java/com/android/server/uwb/UwbContext.java
new file mode 100644
index 0000000..32169a1
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbContext.java
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.util.Log;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Wrapper for context to override getResources method. Resources for uwb mainline jar needs to be
+ * fetched from the resources APK.
+ */
+public class UwbContext extends ContextWrapper {
+    private static final String TAG = "UwbContext";
+    /** Intent action that is used to identify ServiceUwbResources.apk */
+    private static final String ACTION_RESOURCES_APK =
+            "com.android.server.uwb.intent.action.SERVICE_UWB_RESOURCES_APK";
+
+    /** Since service-uwb runs within system_server, its package name is "android". */
+    private static final String SERVICE_UWB_PACKAGE_NAME = "android";
+
+    private String mUwbOverlayApkPkgName;
+
+    // Cached resources from the resources APK.
+    private AssetManager mUwbAssetsFromApk;
+    private Resources mUwbResourcesFromApk;
+    private Resources.Theme mUwbThemeFromApk;
+
+    public UwbContext(@NonNull Context contextBase) {
+        super(contextBase);
+    }
+
+    /** Get the package name of ServiceUwbResources.apk */
+    public String getUwbOverlayApkPkgName() {
+        if (mUwbOverlayApkPkgName != null) {
+            return mUwbOverlayApkPkgName;
+        }
+
+        List<ResolveInfo> resolveInfos = getPackageManager().queryIntentActivities(
+                new Intent(ACTION_RESOURCES_APK),
+                PackageManager.MATCH_SYSTEM_ONLY);
+
+        // remove apps that don't live in the Uwb apex
+        resolveInfos.removeIf(info ->
+                !UwbInjector.isAppInUwbApex(info.activityInfo.applicationInfo));
+
+        if (resolveInfos.isEmpty()) {
+            // Resource APK not loaded yet, print a stack trace to see where this is called from
+            Log.e(TAG, "Attempted to fetch resources before Uwb Resources APK is loaded!",
+                    new IllegalStateException());
+            return null;
+        }
+
+        if (resolveInfos.size() > 1) {
+            // multiple apps found, log a warning, but continue
+            Log.w(TAG, "Found > 1 APK that can resolve Uwb Resources APK intent: "
+                    + resolveInfos.stream()
+                            .map(info -> info.activityInfo.applicationInfo.packageName)
+                            .collect(Collectors.joining(", ")));
+        }
+
+        // Assume the first ResolveInfo is the one we're looking for
+        ResolveInfo info = resolveInfos.get(0);
+        mUwbOverlayApkPkgName = info.activityInfo.applicationInfo.packageName;
+        Log.i(TAG, "Found Uwb Resources APK at: " + mUwbOverlayApkPkgName);
+        return mUwbOverlayApkPkgName;
+    }
+
+    private Context getResourcesApkContext() {
+        try {
+            return createPackageContext(getUwbOverlayApkPkgName(), 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.wtf(TAG, "Failed to load resources", e);
+        }
+        return null;
+    }
+
+    /**
+     * Retrieve assets held in the uwb resources APK.
+     */
+    @Override
+    public AssetManager getAssets() {
+        if (mUwbAssetsFromApk == null) {
+            Context resourcesApkContext = getResourcesApkContext();
+            if (resourcesApkContext != null) {
+                mUwbAssetsFromApk = resourcesApkContext.getAssets();
+            }
+        }
+        return mUwbAssetsFromApk;
+    }
+
+    /**
+     * Retrieve resources held in the uwb resources APK.
+     */
+    @Override
+    public Resources getResources() {
+        if (mUwbResourcesFromApk == null) {
+            Context resourcesApkContext = getResourcesApkContext();
+            if (resourcesApkContext != null) {
+                mUwbResourcesFromApk = resourcesApkContext.getResources();
+            }
+        }
+        return mUwbResourcesFromApk;
+    }
+
+    /**
+     * Retrieve theme held in the uwb resources APK.
+     */
+    @Override
+    public Resources.Theme getTheme() {
+        if (mUwbThemeFromApk == null) {
+            Context resourcesApkContext = getResourcesApkContext();
+            if (resourcesApkContext != null) {
+                mUwbThemeFromApk = resourcesApkContext.getTheme();
+            }
+        }
+        return mUwbThemeFromApk;
+    }
+
+    /** Get the package name that service-uwb runs under. */
+    public String getServiceUwbPackageName() {
+        return SERVICE_UWB_PACKAGE_NAME;
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbCountryCode.java b/service/java/com/android/server/uwb/UwbCountryCode.java
new file mode 100644
index 0000000..058587d
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbCountryCode.java
@@ -0,0 +1,266 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback;
+import android.os.Handler;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.HandlerExecutor;
+import com.android.server.uwb.data.UwbUciConstants;
+import com.android.server.uwb.jni.NativeUwbManager;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Provide functions for making changes to UWB country code.
+ * This Country Code is from MCC or phone default setting. This class sends Country Code
+ * to UWB venodr via the HAL.
+ */
+public class UwbCountryCode {
+    private static final String TAG = "UwbCountryCode";
+    // To be used when there is no country code available.
+    @VisibleForTesting
+    public static final String DEFAULT_COUNTRY_CODE = "00";
+    private static final DateTimeFormatter FORMATTER =
+            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final TelephonyManager mTelephonyManager;
+    private final NativeUwbManager mNativeUwbManager;
+    private final UwbInjector mUwbInjector;
+    private final Set<CountryCodeChangedListener> mListeners = new ArraySet<>();
+
+    private String mTelephonyCountryCode = null;
+    private String mWifiCountryCode = null;
+    private String mOverrideCountryCode = null;
+    private String mCountryCode = null;
+    private String mCountryCodeUpdatedTimestamp = null;
+    private String mTelephonyCountryTimestamp = null;
+    private String mWifiCountryTimestamp = null;
+
+    public interface CountryCodeChangedListener {
+        void onCountryCodeChanged(@Nullable String newCountryCode);
+    }
+
+    public UwbCountryCode(
+            Context context, NativeUwbManager nativeUwbManager, Handler handler,
+            UwbInjector uwbInjector) {
+        mContext = context;
+        mTelephonyManager = context.getSystemService(TelephonyManager.class);
+        mNativeUwbManager = nativeUwbManager;
+        mHandler = handler;
+        mUwbInjector = uwbInjector;
+    }
+
+    private class WifiCountryCodeCallback implements ActiveCountryCodeChangedCallback {
+        public void onActiveCountryCodeChanged(@NonNull String countryCode) {
+            setWifiCountryCode(countryCode);
+        }
+
+        public void onCountryCodeInactive() {
+            setWifiCountryCode("");
+        }
+    }
+
+    /**
+     * Initialize the module.
+     */
+    public void initialize() {
+        mContext.registerReceiver(
+                new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        String countryCode = intent.getStringExtra(
+                                TelephonyManager.EXTRA_NETWORK_COUNTRY);
+                        Log.d(TAG, "Country code changed to :" + countryCode);
+                        setTelephonyCountryCode(countryCode);
+                    }
+                },
+                new IntentFilter(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED),
+                null, mHandler);
+        if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+            mContext.getSystemService(WifiManager.class).registerActiveCountryCodeChangedCallback(
+                    new HandlerExecutor(mHandler), new WifiCountryCodeCallback());
+        }
+
+        Log.d(TAG, "Default country code from system property is "
+                + mUwbInjector.getOemDefaultCountryCode());
+        setTelephonyCountryCode(mTelephonyManager.getNetworkCountryIso());
+        // Current Wifi country code update is sent immediately on registration.
+    }
+
+    public void addListener(@NonNull CountryCodeChangedListener listener) {
+        mListeners.add(listener);
+    }
+
+    private boolean setTelephonyCountryCode(String countryCode) {
+        if (TextUtils.isEmpty(countryCode)
+                && !TextUtils.isEmpty(mTelephonyManager.getNetworkCountryIso())) {
+            Log.i(TAG, "Skip Telephony CC update to empty because there is "
+                    + "an available CC from default active SIM");
+            return false;
+        }
+        Log.d(TAG, "Set telephony country code to: " + countryCode);
+        mTelephonyCountryTimestamp = LocalDateTime.now().format(FORMATTER);
+        // Empty country code.
+        if (TextUtils.isEmpty(countryCode)) {
+            Log.d(TAG, "Received empty telephony country code, reset to default country code");
+            mTelephonyCountryCode = null;
+        } else {
+            mTelephonyCountryCode = countryCode.toUpperCase(Locale.US);
+        }
+        return setCountryCode(false);
+    }
+
+    private boolean setWifiCountryCode(String countryCode) {
+        Log.d(TAG, "Set wifi country code to: " + countryCode);
+        mWifiCountryTimestamp = LocalDateTime.now().format(FORMATTER);
+        // Empty country code.
+        if (TextUtils.isEmpty(countryCode)) {
+            Log.d(TAG, "Received empty wifi country code, reset to default country code");
+            mWifiCountryCode = null;
+        } else {
+            mWifiCountryCode = countryCode.toUpperCase(Locale.US);
+        }
+        return setCountryCode(false);
+    }
+
+    private String pickCountryCode() {
+        if (mOverrideCountryCode != null) {
+            return mOverrideCountryCode;
+        }
+        if (mTelephonyCountryCode != null) {
+            return mTelephonyCountryCode;
+        }
+        if (mWifiCountryCode != null) {
+            return mWifiCountryCode;
+        }
+        return mUwbInjector.getOemDefaultCountryCode();
+    }
+
+    /**
+     * Set country code
+     *
+     * @param forceUpdate Force update the country code even if it was the same as previously cached
+     *                    value.
+     * @return true if the country code is set successfully, false otherwise.
+     */
+    public boolean setCountryCode(boolean forceUpdate) {
+        String country = pickCountryCode();
+        if (country == null) {
+            Log.i(TAG, "No valid country code, reset to " + DEFAULT_COUNTRY_CODE);
+            country = DEFAULT_COUNTRY_CODE;
+        }
+        if (!forceUpdate && Objects.equals(country, mCountryCode)) {
+            Log.i(TAG, "Ignoring already set country code: " + country);
+            return false;
+        }
+        Log.d(TAG, "setCountryCode to " + country);
+        int status = mNativeUwbManager.setCountryCode(country.getBytes(StandardCharsets.UTF_8));
+        boolean success = (status == UwbUciConstants.STATUS_CODE_OK);
+        if (!success) {
+            Log.i(TAG, "Failed to set country code");
+            return false;
+        }
+        mCountryCode = country;
+        mCountryCodeUpdatedTimestamp = LocalDateTime.now().format(FORMATTER);
+        for (CountryCodeChangedListener listener : mListeners) {
+            listener.onCountryCodeChanged(country);
+        }
+        return true;
+    }
+
+    /**
+     * Get country code
+     *
+     * @return true if the country code is set successfully, false otherwise.
+     */
+    public String getCountryCode() {
+        return mCountryCode;
+    }
+
+    /**
+     * Is this a valid country code
+     * @param countryCode A 2-Character alphanumeric country code.
+     * @return true if the countryCode is valid, false otherwise.
+     */
+    public static boolean isValid(String countryCode) {
+        return countryCode != null && countryCode.length() == 2
+                && countryCode.chars().allMatch(Character::isLetterOrDigit);
+    }
+
+    /**
+     * This call will override any existing country code.
+     * This is for test purpose only and we should disallow any update from
+     * telephony in this mode.
+     * @param countryCode A 2-Character alphanumeric country code.
+     */
+    public synchronized void setOverrideCountryCode(String countryCode) {
+        if (TextUtils.isEmpty(countryCode)) {
+            Log.d(TAG, "Fail to override country code because"
+                    + "the received country code is empty");
+            return;
+        }
+        mOverrideCountryCode = countryCode.toUpperCase(Locale.US);
+        setCountryCode(true);
+    }
+
+    /**
+     * This is for clearing the country code previously set through #setOverrideCountryCode() method
+     */
+    public synchronized void clearOverrideCountryCode() {
+        mOverrideCountryCode = null;
+        setCountryCode(true);
+    }
+
+    /**
+     * Method to dump the current state of this UwbCountryCode object.
+     */
+    public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("DefaultCountryCode(system property): "
+                + mUwbInjector.getOemDefaultCountryCode());
+        pw.println("mOverrideCountryCode: " + mOverrideCountryCode);
+        pw.println("mTelephonyCountryCode: " + mTelephonyCountryCode);
+        pw.println("mTelephonyCountryTimestamp: " + mTelephonyCountryTimestamp);
+        pw.println("mWifiCountryCode: " + mWifiCountryCode);
+        pw.println("mWifiCountryTimestamp: " + mWifiCountryTimestamp);
+        pw.println("mCountryCode: " + mCountryCode);
+        pw.println("mCountryCodeUpdatedTimestamp: " + mCountryCodeUpdatedTimestamp);
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbDiagnostics.java b/service/java/com/android/server/uwb/UwbDiagnostics.java
new file mode 100644
index 0000000..173f9eb
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbDiagnostics.java
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import android.content.Context;
+import android.os.BugreportManager;
+import android.os.BugreportParams;
+import android.util.Log;
+
+/**
+ * A class to trigger bugreport and other logs for UWB related failures
+ */
+public class UwbDiagnostics {
+    private static final  String TAG = "UwbDiagnostics";
+    private final Context mContext;
+    private final SystemBuildProperties mSystemBuildProperties;
+    private final UwbInjector mUwbInjector;
+    private long mLastBugReportTimeMs;
+    public UwbDiagnostics(
+            Context context, UwbInjector uwbInjector, SystemBuildProperties systemBuildProperties) {
+        mContext = context;
+        mSystemBuildProperties = systemBuildProperties;
+        mUwbInjector = uwbInjector;
+    }
+
+    /**
+     * Take a bug report if it is not in user build and there is no recent bug report
+     */
+    public void takeBugReport(String bugTitle) {
+        if (mSystemBuildProperties.isUserBuild()) {
+            return;
+        }
+        long currentTimeMs = mUwbInjector.getElapsedSinceBootMillis();
+        if ((currentTimeMs - mLastBugReportTimeMs)
+                < mUwbInjector.getDeviceConfigFacade().getBugReportMinIntervalMs()
+                && mLastBugReportTimeMs > 0) {
+            return;
+        }
+        mLastBugReportTimeMs = currentTimeMs;
+        BugreportManager bugreportManager = mContext.getSystemService(BugreportManager.class);
+        BugreportParams params = new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL);
+        try {
+            bugreportManager.requestBugreport(params, bugTitle, bugTitle);
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Error taking bugreport: " + e);
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbInjector.java b/service/java/com/android/server/uwb/UwbInjector.java
new file mode 100644
index 0000000..70d32b9
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbInjector.java
@@ -0,0 +1,319 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import static android.Manifest.permission.UWB_RANGING;
+import static android.permission.PermissionManager.PERMISSION_GRANTED;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.AlarmManager;
+import android.content.ApexEnvironment;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.permission.PermissionManager;
+import android.provider.Settings;
+import android.util.AtomicFile;
+import android.util.Log;
+
+import com.android.server.uwb.jni.NativeUwbManager;
+import com.android.server.uwb.multchip.UwbMultichipData;
+
+import java.io.File;
+import java.util.Locale;
+
+/**
+ * To be used for dependency injection (especially helps mocking static dependencies).
+ */
+public class UwbInjector {
+    private static final String TAG = "UwbInjector";
+    private static final String APEX_NAME = "com.android.uwb";
+    private static final String VENDOR_SERVICE_NAME = "uwb_vendor";
+    private static final String BOOT_DEFAULT_UWB_COUNTRY_CODE = "ro.boot.uwbcountrycode";
+
+    /**
+     * The path where the Uwb apex is mounted.
+     * Current value = "/apex/com.android.uwb"
+     */
+    private static final String UWB_APEX_PATH =
+            new File("/apex", APEX_NAME).getAbsolutePath();
+    private static final int APP_INFO_FLAGS_SYSTEM_APP =
+            ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
+
+    private final UwbContext mContext;
+    private final Looper mLooper;
+    private final PermissionManager mPermissionManager;
+    private final UwbSettingsStore mUwbSettingsStore;
+    private final NativeUwbManager mNativeUwbManager;
+    private final UwbCountryCode mUwbCountryCode;
+    private final UwbServiceCore mUwbService;
+    private final UwbMetrics mUwbMetrics;
+    private final DeviceConfigFacade mDeviceConfigFacade;
+    private final UwbMultichipData mUwbMultichipData;
+    private final SystemBuildProperties mSystemBuildProperties;
+    private final UwbDiagnostics mUwbDiagnostics;
+
+    public UwbInjector(@NonNull UwbContext context) {
+        // Create UWB service thread.
+        HandlerThread uwbHandlerThread = new HandlerThread("UwbService");
+        uwbHandlerThread.start();
+        mLooper = uwbHandlerThread.getLooper();
+
+        mContext = context;
+        mPermissionManager = context.getSystemService(PermissionManager.class);
+        mUwbSettingsStore = new UwbSettingsStore(
+                context, new Handler(mLooper),
+                new AtomicFile(new File(getDeviceProtectedDataDir(),
+                        UwbSettingsStore.FILE_NAME)), this);
+        mNativeUwbManager = new NativeUwbManager(this);
+        mUwbCountryCode =
+                new UwbCountryCode(mContext, mNativeUwbManager, new Handler(mLooper), this);
+        mUwbMetrics = new UwbMetrics(this);
+        mDeviceConfigFacade = new DeviceConfigFacade(new Handler(mLooper), this);
+        mUwbMultichipData = new UwbMultichipData(mContext);
+        UwbConfigurationManager uwbConfigurationManager =
+                new UwbConfigurationManager(mNativeUwbManager);
+        UwbSessionNotificationManager uwbSessionNotificationManager =
+                new UwbSessionNotificationManager(this);
+        UwbSessionManager uwbSessionManager =
+                new UwbSessionManager(uwbConfigurationManager, mNativeUwbManager, mUwbMetrics,
+                        uwbSessionNotificationManager, this,
+                        mContext.getSystemService(AlarmManager.class), mLooper);
+        mUwbService = new UwbServiceCore(mContext, mNativeUwbManager, mUwbMetrics,
+                mUwbCountryCode, uwbSessionManager, uwbConfigurationManager, this, mLooper);
+        mSystemBuildProperties = new SystemBuildProperties();
+        mUwbDiagnostics = new UwbDiagnostics(mContext, this, mSystemBuildProperties);
+    }
+
+    public UwbSettingsStore getUwbSettingsStore() {
+        return mUwbSettingsStore;
+    }
+
+    public NativeUwbManager getNativeUwbManager() {
+        return mNativeUwbManager;
+    }
+
+    public UwbCountryCode getUwbCountryCode() {
+        return mUwbCountryCode;
+    }
+
+    public UwbMetrics getUwbMetrics() {
+        return mUwbMetrics;
+    }
+
+    public DeviceConfigFacade getDeviceConfigFacade() {
+        return mDeviceConfigFacade;
+    }
+
+    public UwbMultichipData getMultichipData() {
+        return mUwbMultichipData;
+    }
+
+    public UwbServiceCore getUwbServiceCore() {
+        return mUwbService;
+    }
+
+    public UwbDiagnostics getUwbDiagnostics() {
+        return mUwbDiagnostics;
+    }
+
+    /**
+     * Create a UwbShellCommand instance.
+     */
+    public UwbShellCommand makeUwbShellCommand(UwbServiceImpl uwbService) {
+        return new UwbShellCommand(this, uwbService, mContext);
+    }
+
+    /**
+     * Throws security exception if the UWB_RANGING permission is not granted for the calling app.
+     *
+     * <p>Should be used in situations where the app op should not be noted.
+     */
+    public void enforceUwbRangingPermissionForPreflight(
+            @NonNull AttributionSource attributionSource) {
+        if (!attributionSource.checkCallingUid()) {
+            throw new SecurityException("Invalid attribution source " + attributionSource
+                    + ", callingUid: " + Binder.getCallingUid());
+        }
+        int permissionCheckResult = mPermissionManager.checkPermissionForPreflight(
+                UWB_RANGING, attributionSource);
+        if (permissionCheckResult != PERMISSION_GRANTED) {
+            throw new SecurityException("Caller does not hold UWB_RANGING permission");
+        }
+    }
+
+    /**
+     * Returns true if the UWB_RANGING permission is granted for the calling app.
+     *
+     * <p>Should be used in situations where data will be delivered and hence the app op should
+     * be noted.
+     */
+    public boolean checkUwbRangingPermissionForDataDelivery(
+            @NonNull AttributionSource attributionSource, @NonNull String message) {
+        int permissionCheckResult = mPermissionManager.checkPermissionForDataDelivery(
+                UWB_RANGING, attributionSource, message);
+        return permissionCheckResult == PERMISSION_GRANTED;
+    }
+
+    /**
+     * Get device protected storage dir for the UWB apex.
+     */
+    @NonNull
+    public File getDeviceProtectedDataDir() {
+        return ApexEnvironment.getApexEnvironment(APEX_NAME).getDeviceProtectedDataDir();
+    }
+
+    /**
+     * Get integer value from Settings.
+     *
+     * @throws Settings.SettingNotFoundException
+     */
+    public int getSettingsInt(@NonNull String key) throws Settings.SettingNotFoundException {
+        return Settings.Global.getInt(mContext.getContentResolver(), key);
+    }
+
+    /**
+     * Get integer value from Settings.
+     */
+    public int getSettingsInt(@NonNull String key, int defValue) {
+        return Settings.Global.getInt(mContext.getContentResolver(), key, defValue);
+    }
+
+    /**
+     * Returns true if the app is in the Uwb apex, false otherwise.
+     * Checks if the app's path starts with "/apex/com.android.uwb".
+     */
+    public static boolean isAppInUwbApex(ApplicationInfo appInfo) {
+        return appInfo.sourceDir.startsWith(UWB_APEX_PATH);
+    }
+
+    /**
+     * Get the current time of the clock in milliseconds.
+     *
+     * @return Current time in milliseconds.
+     */
+    public long getWallClockMillis() {
+        return System.currentTimeMillis();
+    }
+
+    /**
+     * Returns milliseconds since boot, including time spent in sleep.
+     *
+     * @return Current time since boot in milliseconds.
+     */
+    public long getElapsedSinceBootMillis() {
+        return SystemClock.elapsedRealtime();
+    }
+
+    /**
+     * Returns nanoseconds since boot, including time spent in sleep.
+     *
+     * @return Current time since boot in milliseconds.
+     */
+    public long getElapsedSinceBootNanos() {
+        return SystemClock.elapsedRealtimeNanos();
+    }
+
+    /**
+     * Is this a valid country code
+     * @param countryCode A 2-Character alphanumeric country code.
+     * @return true if the countryCode is valid, false otherwise.
+     */
+    private static boolean isValidCountryCode(String countryCode) {
+        return countryCode != null && countryCode.length() == 2
+                && countryCode.chars().allMatch(Character::isLetterOrDigit);
+    }
+
+    /**
+     * Default country code stored in system property
+     *
+     * @return Country code if available, null otherwise.
+     */
+    public String getOemDefaultCountryCode() {
+        String country = SystemProperties.get(BOOT_DEFAULT_UWB_COUNTRY_CODE);
+        return isValidCountryCode(country) ? country.toUpperCase(Locale.US) : null;
+    }
+
+    /**
+     * Helper method creating a context based on the app's uid (to deal with multi user scenarios)
+     */
+    @Nullable
+    private Context createPackageContextAsUser(int uid) {
+        Context userContext;
+        try {
+            userContext = mContext.createPackageContextAsUser(mContext.getPackageName(), 0,
+                    UserHandle.getUserHandleForUid(uid));
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Unknown package name");
+            return null;
+        }
+        if (userContext == null) {
+            Log.e(TAG, "Unable to retrieve user context for " + uid);
+            return null;
+        }
+        return userContext;
+    }
+
+    /** Helper method to check if the app is a system app. */
+    public boolean isSystemApp(int uid, @NonNull String packageName) {
+        try {
+            ApplicationInfo info = createPackageContextAsUser(uid)
+                    .getPackageManager()
+                    .getApplicationInfo(packageName, 0);
+            return (info.flags & APP_INFO_FLAGS_SYSTEM_APP) != 0;
+        } catch (PackageManager.NameNotFoundException e) {
+            // In case of exception, assume unknown app (more strict checking)
+            // Note: This case will never happen since checkPackage is
+            // called to verify validity before checking App's version.
+            Log.e(TAG, "Failed to get the app info", e);
+        }
+        return false;
+    }
+
+    /** Helper method to retrieve app importance. */
+    private int getPackageImportance(int uid, @NonNull String packageName) {
+        try {
+            return createPackageContextAsUser(uid)
+                    .getSystemService(ActivityManager.class)
+                    .getPackageImportance(packageName);
+        } catch (SecurityException e) {
+            Log.e(TAG, "Failed to retrieve the app importance", e);
+            return ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE;
+        }
+    }
+
+    /** Helper method to check if the app is from foreground app/service. */
+    public boolean isForegroundAppOrService(int uid, @NonNull String packageName) {
+        try {
+            return getPackageImportance(uid, packageName)
+                    <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
+        } catch (SecurityException e) {
+            Log.e(TAG, "Failed to retrieve the app importance", e);
+            return false;
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbMetrics.java b/service/java/com/android/server/uwb/UwbMetrics.java
new file mode 100644
index 0000000..ffbb0c4
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbMetrics.java
@@ -0,0 +1,505 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb;
+
+import android.util.SparseArray;
+import android.uwb.RangingMeasurement;
+
+import com.android.server.uwb.UwbSessionManager.UwbSession;
+import com.android.server.uwb.data.UwbRangingData;
+import com.android.server.uwb.data.UwbTwoWayMeasurement;
+import com.android.server.uwb.data.UwbUciConstants;
+import com.android.server.uwb.proto.UwbStatsLog;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.ccc.CccOpenRangingParams;
+import com.google.uwb.support.fira.FiraOpenSessionParams;
+import com.google.uwb.support.fira.FiraParams;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayDeque;
+import java.util.Calendar;
+import java.util.Deque;
+
+/**
+ * A class to collect and report UWB metrics.
+ */
+public class UwbMetrics {
+    private static final String TAG = "UwbMetrics";
+
+    private static final int MAX_RANGING_SESSIONS = 128;
+    private static final int MAX_RANGING_REPORTS = 1024;
+    public static final int INVALID_DISTANCE = 0xFFFF;
+    private static final int ONE_SECOND_IN_MS = 1000;
+    private static final int TEN_SECOND_IN_MS = 10 * 1000;
+    private static final int ONE_MIN_IN_MS = 60 * 1000;
+    private static final int TEN_MIN_IN_MS = 600 * 1000;
+    private static final int ONE_HOUR_IN_MS = 3600 * 1000;
+    private final UwbInjector mUwbInjector;
+    private final Deque<RangingSessionStats> mRangingSessionList = new ArrayDeque<>();
+    private final SparseArray<RangingSessionStats> mOpenedSessionMap = new SparseArray<>();
+    private final Deque<RangingReportEvent> mRangingReportList = new ArrayDeque<>();
+    private int mNumApps = 0;
+    private long mLastRangingDataLogTimeMs;
+    private final Object mLock = new Object();
+
+    /**
+     * The class storing the stats of a ranging session.
+     */
+    public class RangingSessionStats {
+        private int mSessionId;
+        private int mChannel = 9;
+        private long mStartTimeWallClockMs;
+        private long mStartTimeSinceBootMs;
+        private int mInitLatencyMs;
+        private int mInitStatus;
+        private int mActiveDuration;
+        private int mRangingCount;
+        private int mValidRangingCount;
+        private boolean mHasValidRangingSinceStart;
+        private int mStartCount;
+        private int mStartFailureCount;
+        private int mStartNoValidReportCount;
+        private int mStsType = UwbStatsLog.UWB_SESSION_INITIATED__STS__UNKNOWN_STS;
+        private boolean mIsInitiator;
+        private boolean mIsController;
+        private boolean mIsDiscoveredByFramework = false;
+        private boolean mIsOutOfBand = true;
+
+        RangingSessionStats(int sessionId) {
+            mSessionId = sessionId;
+            mStartTimeWallClockMs = mUwbInjector.getWallClockMillis();
+        }
+
+        /**
+         * Parse UWB profile parameters
+         */
+        public void parseParams(Params params) {
+            if (params instanceof FiraOpenSessionParams) {
+                parseFiraParams((FiraOpenSessionParams) params);
+            } else if (params instanceof CccOpenRangingParams) {
+                parseCccParams((CccOpenRangingParams) params);
+            }
+        }
+
+        private void parseFiraParams(FiraOpenSessionParams params) {
+            if (params.getStsConfig() == FiraParams.STS_CONFIG_STATIC) {
+                mStsType = UwbStatsLog.UWB_SESSION_INITIATED__STS__STATIC;
+            } else if (params.getStsConfig() == FiraParams.STS_CONFIG_DYNAMIC) {
+                mStsType = UwbStatsLog.UWB_SESSION_INITIATED__STS__DYNAMIC;
+            } else {
+                mStsType = UwbStatsLog.UWB_SESSION_INITIATED__STS__PROVISIONED;
+            }
+
+            mIsInitiator = params.getDeviceRole() == FiraParams.RANGING_DEVICE_ROLE_INITIATOR;
+            mIsController = params.getDeviceType() == FiraParams.RANGING_DEVICE_TYPE_CONTROLLER;
+            mChannel = params.getChannelNumber();
+        }
+
+        private void parseCccParams(CccOpenRangingParams params) {
+            mChannel = params.getChannel();
+        }
+
+        private void convertInitStatus(int status) {
+            mInitStatus = UwbStatsLog.UWB_SESSION_INITIATED__STATUS__GENERAL_FAILURE;
+            switch (status) {
+                case UwbUciConstants.STATUS_CODE_OK:
+                    mInitStatus = UwbStatsLog.UWB_SESSION_INITIATED__STATUS__SUCCESS;
+                    break;
+                case UwbUciConstants.STATUS_CODE_ERROR_MAX_SESSIONS_EXCEEDED:
+                    mInitStatus = UwbStatsLog.UWB_SESSION_INITIATED__STATUS__SESSION_EXCEEDED;
+                    break;
+                case UwbUciConstants.STATUS_CODE_ERROR_SESSION_DUPLICATE:
+                    mInitStatus = UwbStatsLog.UWB_SESSION_INITIATED__STATUS__SESSION_DUPLICATE;
+                    break;
+                case UwbUciConstants.STATUS_CODE_INVALID_PARAM:
+                case UwbUciConstants.STATUS_CODE_INVALID_RANGE:
+                case UwbUciConstants.STATUS_CODE_INVALID_MESSAGE_SIZE:
+                    mInitStatus = UwbStatsLog.UWB_SESSION_INITIATED__STATUS__BAD_PARAMS;
+                    break;
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("rangingStartTime=");
+            Calendar c = Calendar.getInstance();
+            synchronized (mLock) {
+                c.setTimeInMillis(mStartTimeWallClockMs);
+                sb.append(mStartTimeWallClockMs == 0 ? "            <null>" :
+                        String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c));
+                sb.append(", sessionId=").append(mSessionId);
+                sb.append(", initLatencyMs=").append(mInitLatencyMs);
+                sb.append(", activeDurationMs=").append(mActiveDuration);
+                sb.append(", rangingCount=").append(mRangingCount);
+                sb.append(", validRangingCount=").append(mValidRangingCount);
+                sb.append(", startCount").append(mStartCount);
+                sb.append(", startFailureCount").append(mStartFailureCount);
+                sb.append(", startNoValidReportCount").append(mStartNoValidReportCount);
+                sb.append(", initStatus=").append(mInitStatus);
+                sb.append(", channel=").append(mChannel);
+                sb.append(", initiator=").append(mIsInitiator);
+                sb.append(", controller=").append(mIsController);
+                sb.append(", discoveredByFramework=").append(mIsDiscoveredByFramework);
+                return sb.toString();
+            }
+        }
+    }
+
+    private class RangingReportEvent {
+        private int mSessionId;
+        private int mNlos;
+        private int mDistanceCm;
+        private int mAzimuthDegree;
+        private int mAzimuthFom;
+        private int mElevationDegree;
+        private int mElevationFom;
+        private long mWallClockMillis;
+
+        RangingReportEvent(int sessionId, int nlos, int distanceCm,
+                int azimuthDegree, int azimuthFom,
+                int elevationDegree, int elevationFom) {
+            mSessionId = sessionId;
+            mWallClockMillis = mUwbInjector.getWallClockMillis();
+            mNlos = nlos;
+            mDistanceCm = distanceCm;
+            mAzimuthDegree = azimuthDegree;
+            mAzimuthFom = azimuthFom;
+            mElevationDegree = elevationDegree;
+            mElevationFom = elevationFom;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("time=");
+            Calendar c = Calendar.getInstance();
+            synchronized (mLock) {
+                c.setTimeInMillis(mWallClockMillis);
+                sb.append(mWallClockMillis == 0 ? "            <null>" :
+                        String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c));
+                sb.append(", sessionId=").append(mSessionId);
+                sb.append(", Nlos=").append(mNlos);
+                sb.append(", DistanceCm=").append(mDistanceCm);
+                sb.append(", AzimuthDegree=").append(mAzimuthDegree);
+                sb.append(", AzimuthFom=").append(mAzimuthFom);
+                sb.append(", ElevationDegree=").append(mElevationDegree);
+                sb.append(", ElevationFom=").append(mElevationFom);
+                return sb.toString();
+            }
+        }
+    }
+
+    public UwbMetrics(UwbInjector uwbInjector) {
+        mUwbInjector = uwbInjector;
+    }
+
+    /**
+     * Log the ranging session initialization event
+     */
+    public void logRangingInitEvent(UwbSession uwbSession, int status) {
+        synchronized (mLock) {
+            // If past maximum events, start removing the oldest
+            while (mRangingSessionList.size() >= MAX_RANGING_SESSIONS) {
+                mRangingSessionList.removeFirst();
+            }
+            RangingSessionStats session = new RangingSessionStats(uwbSession.getSessionId());
+            session.parseParams(uwbSession.getParams());
+            session.convertInitStatus(status);
+            mRangingSessionList.add(session);
+            mOpenedSessionMap.put(uwbSession.getSessionId(), session);
+            UwbStatsLog.write(UwbStatsLog.UWB_SESSION_INITED, uwbSession.getProfileType(),
+                    session.mStsType, session.mIsInitiator,
+                    session.mIsController, session.mIsDiscoveredByFramework, session.mIsOutOfBand,
+                    session.mChannel, session.mInitStatus,
+                    session.mInitLatencyMs, session.mInitLatencyMs / 20);
+        }
+    }
+
+    /**
+     * Log the ranging session start event
+     */
+    public void longRangingStartEvent(UwbSession uwbSession, int status) {
+        synchronized (mLock) {
+            RangingSessionStats session = mOpenedSessionMap.get(uwbSession.getSessionId());
+            if (session == null) {
+                return;
+            }
+            session.mStartCount++;
+            if (status != UwbUciConstants.STATUS_CODE_OK) {
+                session.mStartFailureCount++;
+                session.mStartTimeSinceBootMs = 0;
+                session.mHasValidRangingSinceStart = false;
+                return;
+            }
+            session.mStartTimeSinceBootMs = mUwbInjector.getElapsedSinceBootMillis();
+        }
+    }
+
+    /**
+     * Log the ranging session stop event
+     */
+    public void longRangingStopEvent(UwbSession uwbSession) {
+        synchronized (mLock) {
+            RangingSessionStats session = mOpenedSessionMap.get(uwbSession.getSessionId());
+            if (session == null) {
+                return;
+            }
+            if (session.mStartTimeSinceBootMs == 0) {
+                return;
+            }
+            if (!session.mHasValidRangingSinceStart) {
+                session.mStartNoValidReportCount++;
+            }
+            session.mHasValidRangingSinceStart = false;
+            session.mActiveDuration += (int) (mUwbInjector.getElapsedSinceBootMillis()
+                    - session.mStartTimeSinceBootMs);
+            session.mStartTimeSinceBootMs = 0;
+        }
+    }
+
+    /**
+     * Log the ranging session close event
+     */
+    public void logRangingCloseEvent(UwbSession uwbSession, int status) {
+        synchronized (mLock) {
+            RangingSessionStats session = mOpenedSessionMap.get(uwbSession.getSessionId());
+            if (session == null) {
+                return;
+            }
+            if (status != UwbUciConstants.STATUS_CODE_OK) {
+                return;
+            }
+            // Ranging may close without stop event
+            if (session.mStartTimeSinceBootMs != 0) {
+                session.mActiveDuration += (int) (mUwbInjector.getElapsedSinceBootMillis()
+                        - session.mStartTimeSinceBootMs);
+                if (!session.mHasValidRangingSinceStart) {
+                    session.mStartNoValidReportCount++;
+                }
+                session.mStartTimeSinceBootMs = 0;
+                session.mHasValidRangingSinceStart = false;
+            }
+
+            UwbStatsLog.write(UwbStatsLog.UWB_SESSION_CLOSED, uwbSession.getProfileType(),
+                    session.mStsType, session.mIsInitiator,
+                    session.mIsController, session.mIsDiscoveredByFramework, session.mIsOutOfBand,
+                    session.mActiveDuration, getDurationBucket(session.mActiveDuration),
+                    session.mRangingCount, session.mValidRangingCount,
+                    getCountBucket(session.mRangingCount),
+                    getCountBucket(session.mValidRangingCount),
+                    session.mStartCount,
+                    session.mStartFailureCount,
+                    session.mStartNoValidReportCount);
+            mOpenedSessionMap.delete(uwbSession.getSessionId());
+        }
+    }
+
+    private int getDurationBucket(int durationMs) {
+        if (durationMs <= ONE_SECOND_IN_MS) {
+            return UwbStatsLog.UWB_SESSION_CLOSED__DURATION_BUCKET__WITHIN_ONE_SEC;
+        } else if (durationMs <= TEN_SECOND_IN_MS) {
+            return UwbStatsLog.UWB_SESSION_CLOSED__DURATION_BUCKET__ONE_TO_TEN_SEC;
+        } else if (durationMs <= ONE_MIN_IN_MS) {
+            return UwbStatsLog.UWB_SESSION_CLOSED__DURATION_BUCKET__TEN_SEC_TO_ONE_MIN;
+        } else if (durationMs <= TEN_MIN_IN_MS) {
+            return UwbStatsLog.UWB_SESSION_CLOSED__DURATION_BUCKET__ONE_TO_TEN_MIN;
+        } else if (durationMs <= ONE_HOUR_IN_MS) {
+            return UwbStatsLog.UWB_SESSION_CLOSED__DURATION_BUCKET__TEN_MIN_TO_ONE_HOUR;
+        } else {
+            return UwbStatsLog.UWB_SESSION_CLOSED__DURATION_BUCKET__MORE_THAN_ONE_HOUR;
+        }
+    }
+
+    private int getCountBucket(int count) {
+        if (count <= 0) {
+            return UwbStatsLog.UWB_SESSION_CLOSED__RANGING_COUNT_BUCKET__ZERO;
+        } else if (count <= 5) {
+            return UwbStatsLog.UWB_SESSION_CLOSED__RANGING_COUNT_BUCKET__ONE_TO_FIVE;
+        } else if (count <= 20) {
+            return UwbStatsLog.UWB_SESSION_CLOSED__RANGING_COUNT_BUCKET__FIVE_TO_TWENTY;
+        } else if (count <= 100) {
+            return UwbStatsLog.UWB_SESSION_CLOSED__RANGING_COUNT_BUCKET__TWENTY_TO_ONE_HUNDRED;
+        } else if (count <= 500) {
+            return UwbStatsLog
+                    .UWB_SESSION_CLOSED__RANGING_COUNT_BUCKET__ONE_HUNDRED_TO_FIVE_HUNDRED;
+        } else {
+            return UwbStatsLog.UWB_SESSION_CLOSED__RANGING_COUNT_BUCKET__MORE_THAN_FIVE_HUNDRED;
+        }
+    }
+
+    /**
+     * Log the usage of API from a new App
+     */
+    public void logNewAppUsage() {
+        synchronized (mLock) {
+            mNumApps++;
+        }
+    }
+
+    /**
+     * Log the ranging measurement result
+     */
+    public void logRangingResult(int profileType, UwbRangingData rangingData) {
+        synchronized (mLock) {
+            if (rangingData.getRangingMeasuresType()
+                    != UwbUciConstants.RANGING_MEASUREMENT_TYPE_TWO_WAY
+                    || rangingData.getNoOfRangingMeasures() < 1) {
+                return;
+            }
+
+            UwbTwoWayMeasurement[] uwbTwoWayMeasurement = rangingData.getRangingTwoWayMeasures();
+            UwbTwoWayMeasurement measurement = uwbTwoWayMeasurement[0];
+
+            int sessionId = (int) rangingData.getSessionId();
+            RangingSessionStats session = mOpenedSessionMap.get(sessionId);
+            if (session != null) {
+                session.mRangingCount++;
+            }
+
+            int rangingStatus = measurement.getRangingStatus();
+            if (rangingStatus != UwbUciConstants.STATUS_CODE_OK) {
+                return;
+            }
+
+            if (session != null) {
+                session.mValidRangingCount++;
+                if (!session.mHasValidRangingSinceStart) {
+                    session.mHasValidRangingSinceStart = true;
+                    writeFirstValidRangingResultSinceStart(profileType, session);
+                }
+            }
+            int distanceCm = measurement.getDistance();
+            int azimuthDegree = (int) measurement.getAoaAzimuth();
+            int azimuthFom = measurement.getAoaAzimuthFom();
+            int elevationDegree = (int) measurement.getAoaElevation();
+            int elevationFom = measurement.getAoaElevationFom();
+            int nlos = getNlos(measurement);
+
+            while (mRangingReportList.size() >= MAX_RANGING_REPORTS) {
+                mRangingReportList.removeFirst();
+            }
+            RangingReportEvent report = new RangingReportEvent(sessionId, nlos, distanceCm,
+                    azimuthDegree, azimuthFom, elevationDegree, elevationFom);
+            mRangingReportList.add(report);
+
+            long currTimeMs = mUwbInjector.getElapsedSinceBootMillis();
+            if ((currTimeMs - mLastRangingDataLogTimeMs) < mUwbInjector.getDeviceConfigFacade()
+                    .getRangingResultLogIntervalMs()) {
+                return;
+            }
+            mLastRangingDataLogTimeMs = currTimeMs;
+
+            boolean isDistanceValid = distanceCm != INVALID_DISTANCE;
+            boolean isAzimuthValid = azimuthFom > 0;
+            boolean isElevationValid = elevationFom > 0;
+            int distance50Cm = isDistanceValid ? distanceCm / 50 : 0;
+            int azimuth10Degree = isAzimuthValid ? azimuthDegree / 10 : 0;
+            int elevation10Degree = isElevationValid ? elevationDegree / 10 : 0;
+            UwbStatsLog.write(UwbStatsLog.UWB_RANGING_MEASUREMENT_RECEIVED, profileType, nlos,
+                    isDistanceValid, distanceCm, distance50Cm, RangingMeasurement.RSSI_UNKNOWN,
+                    isAzimuthValid, azimuthDegree, azimuth10Degree, azimuthFom,
+                    isElevationValid, elevationDegree, elevation10Degree, elevationFom);
+        }
+    }
+
+    private void writeFirstValidRangingResultSinceStart(int profileType,
+            RangingSessionStats session) {
+        int latencyMs = (int) (mUwbInjector.getElapsedSinceBootMillis()
+                - session.mStartTimeSinceBootMs);
+        UwbStatsLog.write(UwbStatsLog.UWB_FIRST_RANGING_RECEIVED,
+                profileType, latencyMs, latencyMs / 200);
+    }
+
+    private int getNlos(UwbTwoWayMeasurement measurement) {
+        int nlos = measurement.getNLoS();
+        if (nlos == 0) {
+            return UwbStatsLog.UWB_RANGING_MEASUREMENT_RECEIVED__NLOS__LOS;
+        } else if (nlos == 1) {
+            return UwbStatsLog.UWB_RANGING_MEASUREMENT_RECEIVED__NLOS__NLOS;
+        } else {
+            return UwbStatsLog.UWB_RANGING_MEASUREMENT_RECEIVED__NLOS__NLOS_UNKNOWN;
+        }
+    }
+
+    private int mNumDeviceInitSuccess = 0;
+    private int mNumDeviceInitFailure = 0;
+    private int mNumDeviceStatusError = 0;
+    private int mNumUciGenericError = 0;
+
+    /**
+     * Increment the count of device initialization success
+     */
+    public synchronized void incrementDeviceInitSuccessCount() {
+        mNumDeviceInitSuccess++;
+    }
+
+    /**
+     * Increment the count of device initialization failure
+     */
+    public synchronized void incrementDeviceInitFailureCount() {
+        mNumDeviceInitFailure++;
+        UwbStatsLog.write(UwbStatsLog.UWB_DEVICE_ERROR_REPORTED,
+                UwbStatsLog.UWB_DEVICE_ERROR_REPORTED__TYPE__INIT_ERROR);
+    }
+
+    /**
+     * Increment the count of device status error
+     */
+    public synchronized void incrementDeviceStatusErrorCount() {
+        mNumDeviceStatusError++;
+        UwbStatsLog.write(UwbStatsLog.UWB_DEVICE_ERROR_REPORTED,
+                UwbStatsLog.UWB_DEVICE_ERROR_REPORTED__TYPE__DEVICE_STATUS_ERROR);
+    }
+
+    /**
+     * Increment the count of UCI generic error which will trigger UCI command retry
+     */
+    public synchronized void incrementUciGenericErrorCount() {
+        mNumUciGenericError++;
+        UwbStatsLog.write(UwbStatsLog.UWB_DEVICE_ERROR_REPORTED,
+                UwbStatsLog.UWB_DEVICE_ERROR_REPORTED__TYPE__UCI_GENERIC_ERROR);
+    }
+
+    /**
+     * Dump the UWB logs
+     */
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        synchronized (mLock) {
+            pw.println("---- Dump of UwbMetrics ----");
+            pw.println("---- mRangingSessionList ----");
+            for (RangingSessionStats stats: mRangingSessionList) {
+                pw.println(stats.toString());
+            }
+            pw.println("---- mOpenedSessionMap ----");
+            for (int i = 0; i < mOpenedSessionMap.size(); i++) {
+                pw.println(mOpenedSessionMap.valueAt(i).toString());
+            }
+            pw.println("---- mRangingReportList ----");
+            for (RangingReportEvent event: mRangingReportList) {
+                pw.println(event.toString());
+            }
+            pw.println("mNumApps=" + mNumApps);
+            pw.println("---- Device operation success/error count ----");
+            pw.println("mNumDeviceInitSuccess = " + mNumDeviceInitSuccess);
+            pw.println("mNumDeviceInitFailure = " + mNumDeviceInitFailure);
+            pw.println("mNumDeviceStatusError = " + mNumDeviceStatusError);
+            pw.println("mNumUciGenericError = " + mNumUciGenericError);
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbService.java b/service/java/com/android/server/uwb/UwbService.java
new file mode 100644
index 0000000..f590c3a
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbService.java
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.server.SystemService;
+
+/**
+ * Uwb System service.
+ */
+public class UwbService extends SystemService {
+    private static final String TAG = "UwbService";
+
+    private final UwbServiceImpl mImpl;
+
+    public UwbService(Context context) {
+        super(context);
+        mImpl = new UwbServiceImpl(context, new UwbInjector(new UwbContext(context)));
+    }
+
+    @Override
+    public void onStart() {
+        Log.i(TAG, "Registering " + Context.UWB_SERVICE);
+        publishBinderService(Context.UWB_SERVICE, mImpl);
+    }
+
+    @Override
+    public void onBootPhase(int phase) {
+        if (phase == SystemService.PHASE_BOOT_COMPLETED) {
+            mImpl.initialize();
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbServiceCore.java b/service/java/com/android/server/uwb/UwbServiceCore.java
new file mode 100644
index 0000000..4b55d78
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbServiceCore.java
@@ -0,0 +1,630 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import static com.google.uwb.support.fira.FiraParams.MULTICAST_LIST_UPDATE_ACTION_ADD;
+import static com.google.uwb.support.fira.FiraParams.MULTICAST_LIST_UPDATE_ACTION_DELETE;
+
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Pair;
+import android.uwb.IUwbAdapterStateCallbacks;
+import android.uwb.IUwbRangingCallbacks;
+import android.uwb.IUwbVendorUciCallback;
+import android.uwb.RangingChangeReason;
+import android.uwb.SessionHandle;
+import android.uwb.StateChangeReason;
+import android.uwb.UwbManager.AdapterStateCallback;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.server.uwb.data.UwbUciConstants;
+import com.android.server.uwb.data.UwbVendorUciResponse;
+import com.android.server.uwb.jni.INativeUwbManager;
+import com.android.server.uwb.jni.NativeUwbManager;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.ccc.CccOpenRangingParams;
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.ccc.CccRangingReconfiguredParams;
+import com.google.uwb.support.ccc.CccStartRangingParams;
+import com.google.uwb.support.fira.FiraControleeParams;
+import com.google.uwb.support.fira.FiraOpenSessionParams;
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraRangingReconfigureParams;
+import com.google.uwb.support.generic.GenericParams;
+import com.google.uwb.support.generic.GenericSpecificationParams;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Core UWB stack.
+ */
+public class UwbServiceCore implements INativeUwbManager.DeviceNotification,
+        INativeUwbManager.VendorNotification, UwbCountryCode.CountryCodeChangedListener {
+    private static final String TAG = "UwbServiceCore";
+
+    private static final int TASK_ENABLE = 1;
+    private static final int TASK_DISABLE = 2;
+
+    private static final int WATCHDOG_MS = 10000;
+    private static final int SEND_VENDOR_CMD_TIMEOUT_MS = 10000;
+
+    private final PowerManager.WakeLock mUwbWakeLock;
+    private final Context mContext;
+    // TODO: Use RemoteCallbackList instead.
+    private final ConcurrentHashMap<Integer, AdapterInfo> mAdapterMap = new ConcurrentHashMap<>();
+    private final EnableDisableTask mEnableDisableTask;
+
+    private final UwbSessionManager mSessionManager;
+    private final UwbConfigurationManager mConfigurationManager;
+    private final NativeUwbManager mNativeUwbManager;
+    private final UwbMetrics mUwbMetrics;
+    private final UwbCountryCode mUwbCountryCode;
+    private final UwbInjector mUwbInjector;
+    private /* @UwbManager.AdapterStateCallback.State */ int mState;
+    private @StateChangeReason int mLastStateChangedReason;
+    private  IUwbVendorUciCallback mCallBack = null;
+
+    public UwbServiceCore(Context uwbApplicationContext, NativeUwbManager nativeUwbManager,
+            UwbMetrics uwbMetrics, UwbCountryCode uwbCountryCode,
+            UwbSessionManager uwbSessionManager, UwbConfigurationManager uwbConfigurationManager,
+            UwbInjector uwbInjector, Looper serviceLooper) {
+        mContext = uwbApplicationContext;
+
+        Log.d(TAG, "Starting Uwb");
+
+        mUwbWakeLock = mContext.getSystemService(PowerManager.class).newWakeLock(
+                PowerManager.PARTIAL_WAKE_LOCK, "UwbServiceCore:mUwbWakeLock");
+
+        mNativeUwbManager = nativeUwbManager;
+
+        mNativeUwbManager.setDeviceListener(this);
+        mNativeUwbManager.setVendorListener(this);
+        mUwbMetrics = uwbMetrics;
+        mUwbCountryCode = uwbCountryCode;
+        mUwbCountryCode.addListener(this);
+        mSessionManager = uwbSessionManager;
+        mConfigurationManager = uwbConfigurationManager;
+        mUwbInjector = uwbInjector;
+
+        updateState(AdapterStateCallback.STATE_DISABLED, StateChangeReason.SYSTEM_BOOT);
+
+        mEnableDisableTask = new EnableDisableTask(serviceLooper);
+    }
+
+    private void updateState(int state, int reason) {
+        synchronized (UwbServiceCore.this) {
+            mState = state;
+            mLastStateChangedReason = reason;
+        }
+    }
+
+    private boolean isUwbEnabled() {
+        synchronized (UwbServiceCore.this) {
+            return (mState == AdapterStateCallback.STATE_ENABLED_ACTIVE
+                    || mState == AdapterStateCallback.STATE_ENABLED_INACTIVE);
+        }
+    }
+
+    String getDeviceStateString(int state) {
+        String ret = "";
+        switch (state) {
+            case UwbUciConstants.DEVICE_STATE_OFF:
+                ret = "OFF";
+                break;
+            case UwbUciConstants.DEVICE_STATE_READY:
+                ret = "READY";
+                break;
+            case UwbUciConstants.DEVICE_STATE_ACTIVE:
+                ret = "ACTIVE";
+                break;
+            case UwbUciConstants.DEVICE_STATE_ERROR:
+                ret = "ERROR";
+                break;
+        }
+        return ret;
+    }
+
+    @Override
+    public void onVendorUciNotificationReceived(int gid, int oid, byte[] payload) {
+        Log.i(TAG, "onVendorUciNotificationReceived");
+        if (mCallBack != null) {
+            try {
+                mCallBack.onVendorNotificationReceived(gid, oid, payload);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to send vendor notification", e);
+            }
+        }
+    }
+
+    @Override
+    public void onDeviceStatusNotificationReceived(int deviceState) {
+        // If error status is received, toggle UWB off to reset stack state.
+        // TODO(b/227488208): Should we try to restart (like wifi) instead?
+        if ((byte) deviceState == UwbUciConstants.DEVICE_STATE_ERROR) {
+            Log.e(TAG, "Error device status received. Disabling...");
+            mUwbMetrics.incrementDeviceStatusErrorCount();
+            takBugReportAfterDeviceError("UWB is disabled due to device status error");
+            setEnabled(false);
+            return;
+        }
+        handleDeviceStatusNotification(deviceState);
+    }
+
+    void handleDeviceStatusNotification(int deviceState) {
+        Log.i(TAG, "handleDeviceStatusNotification = " + getDeviceStateString(deviceState));
+        int state = AdapterStateCallback.STATE_DISABLED;
+        int reason = StateChangeReason.UNKNOWN;
+
+        if (deviceState == UwbUciConstants.DEVICE_STATE_OFF) {
+            state = AdapterStateCallback.STATE_DISABLED;
+            reason = StateChangeReason.SYSTEM_POLICY;
+        } else if (deviceState == UwbUciConstants.DEVICE_STATE_READY) {
+            state = AdapterStateCallback.STATE_ENABLED_INACTIVE;
+            reason = StateChangeReason.SYSTEM_POLICY;
+        } else if (deviceState == UwbUciConstants.DEVICE_STATE_ACTIVE) {
+            state = AdapterStateCallback.STATE_ENABLED_ACTIVE;
+            reason = StateChangeReason.SESSION_STARTED;
+        }
+
+        updateState(state, reason);
+
+        for (AdapterInfo adapter : mAdapterMap.values()) {
+            try {
+                adapter.getAdapterStateCallbacks().onAdapterStateChanged(state, reason);
+            } catch (RemoteException e) {
+                Log.e(TAG, "onAdapterStateChanged is failed");
+            }
+        }
+    }
+
+    @Override
+    public void onCoreGenericErrorNotificationReceived(int status) {
+        Log.e(TAG, "onCoreGenericErrorNotificationReceived status = " + status);
+        mUwbMetrics.incrementUciGenericErrorCount();
+    }
+
+    @Override
+    public void onCountryCodeChanged(@Nullable String countryCode) { }
+
+    public void registerAdapterStateCallbacks(IUwbAdapterStateCallbacks adapterStateCallbacks)
+            throws RemoteException {
+        AdapterInfo adapter = new AdapterInfo(Binder.getCallingPid(), adapterStateCallbacks);
+        mAdapterMap.put(Binder.getCallingPid(), adapter);
+        adapter.getBinder().linkToDeath(adapter, 0);
+        adapterStateCallbacks.onAdapterStateChanged(mState, mLastStateChangedReason);
+    }
+
+    public void unregisterAdapterStateCallbacks(IUwbAdapterStateCallbacks callbacks) {
+        int pid = Binder.getCallingPid();
+        AdapterInfo adapter = mAdapterMap.get(pid);
+        adapter.getBinder().unlinkToDeath(adapter, 0);
+        mAdapterMap.remove(pid);
+    }
+
+    public void registerVendorExtensionCallback(IUwbVendorUciCallback callbacks) {
+        Log.e(TAG, "Register the callback");
+        mCallBack = callbacks;
+    }
+
+    public void unregisterVendorExtensionCallback(IUwbVendorUciCallback callbacks) {
+        Log.e(TAG, "Unregister the callback");
+        mCallBack = null;
+    }
+
+    public PersistableBundle getSpecificationInfo() {
+        // TODO(b/211445008): Consolidate to a single uwb thread.
+        Pair<Integer, GenericSpecificationParams> specificationParams =
+                mConfigurationManager.getCapsInfo(
+                        GenericParams.PROTOCOL_NAME, GenericSpecificationParams.class);
+        if (specificationParams.first != UwbUciConstants.STATUS_CODE_OK)  {
+            Log.e(TAG, "Failed to retrieve specification params");
+            return new PersistableBundle();
+        }
+        return specificationParams.second.toBundle();
+    }
+
+    public long getTimestampResolutionNanos() {
+        return mNativeUwbManager.getTimestampResolutionNanos();
+    }
+
+    /**
+     * Check the attribution source chain to ensure that there are no 3p apps which are not in fg
+     * which can receive the ranging results.
+     * @return true if there is some non-system app which is in not in fg, false otherwise.
+     */
+    private boolean hasAnyNonSystemAppNotInFgInAttributionSource(
+            @NonNull AttributionSource attributionSource) {
+        // Iterate attribution source chain to ensure that there is no non-fg 3p app in the
+        // request.
+        while (attributionSource != null) {
+            int uid = attributionSource.getUid();
+            String packageName = attributionSource.getPackageName();
+            if (!mUwbInjector.isSystemApp(uid, packageName)) {
+                if (!mUwbInjector.isForegroundAppOrService(uid, packageName)) {
+                    Log.e(TAG, "Found a non fg app/service in the attribution source of request: "
+                            + attributionSource);
+                    return true;
+                }
+            }
+            attributionSource = attributionSource.getNext();
+        }
+        return false;
+    }
+
+    public void openRanging(
+            AttributionSource attributionSource,
+            SessionHandle sessionHandle,
+            IUwbRangingCallbacks rangingCallbacks,
+            PersistableBundle params) throws RemoteException {
+        if (!isUwbEnabled()) {
+            throw new IllegalStateException("Uwb is not enabled");
+        }
+        if (hasAnyNonSystemAppNotInFgInAttributionSource(attributionSource)) {
+            Log.e(TAG, "openRanging - System policy disallows");
+            rangingCallbacks.onRangingOpenFailed(sessionHandle,
+                    RangingChangeReason.SYSTEM_POLICY, new PersistableBundle());
+            return;
+        }
+        int sessionId = 0;
+        if (FiraParams.isCorrectProtocol(params)) {
+            FiraOpenSessionParams firaOpenSessionParams = FiraOpenSessionParams.fromBundle(
+                    params);
+            sessionId = firaOpenSessionParams.getSessionId();
+            mSessionManager.initSession(attributionSource, sessionHandle, sessionId,
+                    firaOpenSessionParams.getProtocolName(),
+                    firaOpenSessionParams, rangingCallbacks);
+        } else if (CccParams.isCorrectProtocol(params)) {
+            CccOpenRangingParams cccOpenRangingParams = CccOpenRangingParams.fromBundle(params);
+            sessionId = cccOpenRangingParams.getSessionId();
+            mSessionManager.initSession(attributionSource, sessionHandle, sessionId,
+                    cccOpenRangingParams.getProtocolName(),
+                    cccOpenRangingParams, rangingCallbacks);
+        } else {
+            Log.e(TAG, "openRanging - Wrong parameters");
+            try {
+                rangingCallbacks.onRangingOpenFailed(sessionHandle,
+                        RangingChangeReason.BAD_PARAMETERS, new PersistableBundle());
+            } catch (RemoteException e) { }
+        }
+    }
+
+    public void startRanging(SessionHandle sessionHandle, PersistableBundle params)
+            throws IllegalStateException {
+        if (!isUwbEnabled()) {
+            throw new IllegalStateException("Uwb is not enabled");
+        }
+        Params  startRangingParams = null;
+        if (CccParams.isCorrectProtocol(params)) {
+            startRangingParams = CccStartRangingParams.fromBundle(params);
+        }
+        mSessionManager.startRanging(sessionHandle, startRangingParams);
+        return;
+    }
+
+    public void reconfigureRanging(SessionHandle sessionHandle, PersistableBundle params) {
+        if (!isUwbEnabled()) {
+            throw new IllegalStateException("Uwb is not enabled");
+        }
+        Params  reconfigureRangingParams = null;
+        if (FiraParams.isCorrectProtocol(params)) {
+            reconfigureRangingParams = FiraRangingReconfigureParams.fromBundle(params);
+        } else if (CccParams.isCorrectProtocol(params)) {
+            reconfigureRangingParams = CccRangingReconfiguredParams.fromBundle(params);
+        }
+        mSessionManager.reconfigure(sessionHandle, reconfigureRangingParams);
+    }
+
+    public void stopRanging(SessionHandle sessionHandle) {
+        if (!isUwbEnabled()) {
+            throw new IllegalStateException("Uwb is not enabled");
+        }
+        mSessionManager.stopRanging(sessionHandle);
+    }
+
+    public void closeRanging(SessionHandle sessionHandle) {
+        if (!isUwbEnabled()) {
+            throw new IllegalStateException("Uwb is not enabled");
+        }
+        mSessionManager.deInitSession(sessionHandle);
+    }
+
+    public void addControlee(SessionHandle sessionHandle, PersistableBundle params) {
+        if (!isUwbEnabled()) {
+            throw new IllegalStateException("Uwb is not enabled");
+        }
+        Params  reconfigureRangingParams = null;
+        if (FiraParams.isCorrectProtocol(params)) {
+            FiraControleeParams controleeParams = FiraControleeParams.fromBundle(params);
+            reconfigureRangingParams = new FiraRangingReconfigureParams.Builder()
+                    .setAction(MULTICAST_LIST_UPDATE_ACTION_ADD)
+                    .setAddressList(controleeParams.getAddressList())
+                    .setSubSessionIdList(controleeParams.getSubSessionIdList())
+                    .build();
+        }
+        mSessionManager.reconfigure(sessionHandle, reconfigureRangingParams);
+    }
+
+    public void removeControlee(SessionHandle sessionHandle, PersistableBundle params) {
+        if (!isUwbEnabled()) {
+            throw new IllegalStateException("Uwb is not enabled");
+        }
+        Params  reconfigureRangingParams = null;
+        if (FiraParams.isCorrectProtocol(params)) {
+            FiraControleeParams controleeParams = FiraControleeParams.fromBundle(params);
+            reconfigureRangingParams = new FiraRangingReconfigureParams.Builder()
+                    .setAction(MULTICAST_LIST_UPDATE_ACTION_DELETE)
+                    .setAddressList(controleeParams.getAddressList())
+                    .setSubSessionIdList(controleeParams.getSubSessionIdList())
+                    .build();
+        }
+        mSessionManager.reconfigure(sessionHandle, reconfigureRangingParams);
+    }
+
+    public /* @UwbManager.AdapterStateCallback.State */ int getAdapterState() {
+        synchronized (UwbServiceCore.this) {
+            return mState;
+        }
+    }
+
+    public synchronized void setEnabled(boolean enabled) {
+        int task = enabled ? TASK_ENABLE : TASK_DISABLE;
+
+        if (enabled && isUwbEnabled()) {
+            Log.w(TAG, "Uwb is already enabled");
+        } else if (!enabled && !isUwbEnabled()) {
+            Log.w(TAG, "Uwb is already disabled");
+        }
+
+        mEnableDisableTask.execute(task);
+    }
+
+    private void sendVendorUciResponse(int gid, int oid, byte[] payload) {
+        Log.i(TAG, "onVendorUciResponseReceived");
+        if (mCallBack != null) {
+            try {
+                mCallBack.onVendorResponseReceived(gid, oid, payload);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to send vendor response", e);
+            }
+        }
+    }
+
+    public synchronized int sendVendorUciMessage(int gid, int oid, byte[] payload) {
+        if ((!isUwbEnabled())) {
+            Log.e(TAG, "sendRawVendor : Uwb is not enabled");
+            return UwbUciConstants.STATUS_CODE_FAILED;
+        }
+        // TODO(b/211445008): Consolidate to a single uwb thread.
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        FutureTask<Byte> sendVendorCmdTask = new FutureTask<>(
+                () -> {
+                    UwbVendorUciResponse response =
+                            mNativeUwbManager.sendRawVendorCmd(gid, oid, payload);
+                    if (response.status == UwbUciConstants.STATUS_CODE_OK) {
+                        sendVendorUciResponse(response.gid, response.oid, response.payload);
+                    }
+                    return response.status;
+                });
+        executor.submit(sendVendorCmdTask);
+        int status = UwbUciConstants.STATUS_CODE_FAILED;
+        try {
+            status = sendVendorCmdTask.get(
+                    SEND_VENDOR_CMD_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        } catch (TimeoutException e) {
+            executor.shutdownNow();
+            Log.i(TAG, "Failed to send vendor command - status : TIMEOUT");
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        } catch (ExecutionException e) {
+            e.printStackTrace();
+        }
+        return status;
+    }
+
+    private class EnableDisableTask extends Handler {
+
+        EnableDisableTask(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            int type = msg.what;
+            switch (type) {
+                case TASK_ENABLE:
+                    enableInternal();
+                    break;
+
+                case TASK_DISABLE:
+                    mSessionManager.deinitAllSession();
+                    disableInternal();
+                    break;
+                default:
+                    Log.d(TAG, "EnableDisableTask : Undefined Task");
+                    break;
+            }
+        }
+
+        public void execute(int task) {
+            Message msg = mEnableDisableTask.obtainMessage();
+            msg.what = task;
+            this.sendMessage(msg);
+        }
+
+        private void enableInternal() {
+            if (isUwbEnabled()) {
+                Log.i(TAG, "UWB service is already enabled");
+                return;
+            }
+            try {
+                WatchDogThread watchDog = new WatchDogThread("enableInternal", WATCHDOG_MS);
+                watchDog.start();
+
+                Log.i(TAG, "Initialization start ...");
+                mUwbWakeLock.acquire();
+                try {
+                    if (!mNativeUwbManager.doInitialize()) {
+                        Log.e(TAG, "Error enabling UWB");
+                        mUwbMetrics.incrementDeviceInitFailureCount();
+                        takBugReportAfterDeviceError("Error enabling UWB");
+                        updateState(AdapterStateCallback.STATE_DISABLED,
+                                StateChangeReason.SYSTEM_POLICY);
+                    } else {
+                        Log.i(TAG, "Initialization success");
+                        /* TODO : keep it until MW, FW fix b/196943897 */
+                        mUwbMetrics.incrementDeviceInitSuccessCount();
+                        handleDeviceStatusNotification(UwbUciConstants.DEVICE_STATE_READY);
+                        // Set country code on every enable.
+                        mUwbCountryCode.setCountryCode(true);
+                    }
+                } finally {
+                    mUwbWakeLock.release();
+                    watchDog.cancel();
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+
+        private void disableInternal() {
+            if (!isUwbEnabled()) {
+                Log.i(TAG, "UWB service is already disabled");
+                return;
+            }
+
+            WatchDogThread watchDog = new WatchDogThread("disableInternal", WATCHDOG_MS);
+            watchDog.start();
+
+            try {
+                updateState(AdapterStateCallback.STATE_DISABLED, StateChangeReason.SYSTEM_POLICY);
+                Log.i(TAG, "Deinitialization start ...");
+                mUwbWakeLock.acquire();
+
+                if (!mNativeUwbManager.doDeinitialize()) {
+                    Log.w(TAG, "Error disabling UWB");
+                } else {
+                    Log.i(TAG, "Deinitialization success");
+                    /* UWBS_STATUS_OFF is not the valid state. so handle device state directly */
+                    handleDeviceStatusNotification(UwbUciConstants.DEVICE_STATE_OFF);
+                }
+            } finally {
+                mUwbWakeLock.release();
+                watchDog.cancel();
+            }
+        }
+
+        public class WatchDogThread extends Thread {
+            final Object mCancelWaiter = new Object();
+            final int mTimeout;
+            boolean mCanceled = false;
+
+            WatchDogThread(String threadName, int timeout) {
+                super(threadName);
+
+                mTimeout = timeout;
+            }
+
+            @Override
+            public void run() {
+                try {
+                    synchronized (mCancelWaiter) {
+                        mCancelWaiter.wait(mTimeout);
+                        if (mCanceled) {
+                            return;
+                        }
+                    }
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                    interrupt();
+                }
+
+                if (mUwbWakeLock.isHeld()) {
+                    Log.e(TAG, "Release mUwbWakeLock before aborting.");
+                    mUwbWakeLock.release();
+                }
+            }
+
+            public synchronized void cancel() {
+                synchronized (mCancelWaiter) {
+                    mCanceled = true;
+                    mCancelWaiter.notify();
+                }
+            }
+        }
+    }
+
+    class AdapterInfo implements IBinder.DeathRecipient {
+        private final IBinder mIBinder;
+        private IUwbAdapterStateCallbacks mAdapterStateCallbacks;
+        private int mPid;
+
+        AdapterInfo(int pid, IUwbAdapterStateCallbacks adapterStateCallbacks) {
+            mIBinder = adapterStateCallbacks.asBinder();
+            mAdapterStateCallbacks = adapterStateCallbacks;
+            mPid = pid;
+        }
+
+        public IUwbAdapterStateCallbacks getAdapterStateCallbacks() {
+            return mAdapterStateCallbacks;
+        }
+
+        public IBinder getBinder() {
+            return mIBinder;
+        }
+
+        @Override
+        public void binderDied() {
+            mIBinder.unlinkToDeath(this, 0);
+            mAdapterMap.remove(mPid);
+        }
+    }
+
+    private void takBugReportAfterDeviceError(String bugTitle) {
+        if (mUwbInjector.getDeviceConfigFacade().isDeviceErrorBugreportEnabled()) {
+            mUwbInjector.getUwbDiagnostics().takeBugReport(bugTitle);
+        }
+    }
+
+    /**
+     * Dump the UWB service status
+     */
+    public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("---- Dump of UwbServiceCore ----");
+        pw.println("device state = " + getDeviceStateString(mState));
+        pw.println("mLastStateChangedReason = " + mLastStateChangedReason);
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbServiceImpl.java b/service/java/com/android/server/uwb/UwbServiceImpl.java
new file mode 100644
index 0000000..ad4c949
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbServiceImpl.java
@@ -0,0 +1,372 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import android.annotation.NonNull;
+import android.content.AttributionSource;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+import android.uwb.IUwbAdapter;
+import android.uwb.IUwbAdapterStateCallbacks;
+import android.uwb.IUwbAdfProvisionStateCallbacks;
+import android.uwb.IUwbRangingCallbacks;
+import android.uwb.IUwbVendorUciCallback;
+import android.uwb.SessionHandle;
+import android.uwb.UwbAddress;
+
+import com.google.uwb.support.multichip.ChipInfoParams;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implementation of {@link android.uwb.IUwbAdapter} binder service.
+ */
+public class UwbServiceImpl extends IUwbAdapter.Stub {
+    private static final String TAG = "UwbServiceImpl";
+
+    private final Context mContext;
+    private final UwbInjector mUwbInjector;
+    private final UwbSettingsStore mUwbSettingsStore;
+    private final UwbServiceCore mUwbServiceCore;
+
+
+    UwbServiceImpl(@NonNull Context context, @NonNull UwbInjector uwbInjector) {
+        mContext = context;
+        mUwbInjector = uwbInjector;
+        mUwbSettingsStore = uwbInjector.getUwbSettingsStore();
+        mUwbServiceCore = uwbInjector.getUwbServiceCore();
+        registerAirplaneModeReceiver();
+    }
+
+    /**
+     * Initialize the stack after boot completed.
+     */
+    public void initialize() {
+        mUwbSettingsStore.initialize();
+        mUwbInjector.getMultichipData().initialize();
+        mUwbInjector.getUwbCountryCode().initialize();
+        // Initialize the UCI stack at bootup.
+        mUwbServiceCore.setEnabled(isUwbEnabled());
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                != PERMISSION_GRANTED) {
+            pw.println("Permission Denial: can't dump UwbService from from pid="
+                    + Binder.getCallingPid()
+                    + ", uid=" + Binder.getCallingUid());
+            return;
+        }
+        mUwbSettingsStore.dump(fd, pw, args);
+        mUwbInjector.getUwbMetrics().dump(fd, pw, args);
+        mUwbServiceCore.dump(fd, pw, args);
+        mUwbInjector.getUwbCountryCode().dump(fd, pw, args);
+    }
+
+    private void enforceUwbPrivilegedPermission() {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.UWB_PRIVILEGED,
+                "UwbService");
+    }
+
+    @Override
+    public void registerAdapterStateCallbacks(IUwbAdapterStateCallbacks adapterStateCallbacks)
+            throws RemoteException {
+        enforceUwbPrivilegedPermission();
+        mUwbServiceCore.registerAdapterStateCallbacks(adapterStateCallbacks);
+    }
+
+    @Override
+    public void registerVendorExtensionCallback(IUwbVendorUciCallback callbacks)
+            throws RemoteException {
+        Log.i(TAG, "Register the callback");
+        enforceUwbPrivilegedPermission();
+        mUwbServiceCore.registerVendorExtensionCallback(callbacks);
+    }
+
+    @Override
+    public void unregisterVendorExtensionCallback(IUwbVendorUciCallback callbacks)
+            throws RemoteException {
+        Log.i(TAG, "Unregister the callback");
+        enforceUwbPrivilegedPermission();
+        mUwbServiceCore.unregisterVendorExtensionCallback(callbacks);
+    }
+
+
+    @Override
+    public void unregisterAdapterStateCallbacks(IUwbAdapterStateCallbacks adapterStateCallbacks)
+            throws RemoteException {
+        enforceUwbPrivilegedPermission();
+        mUwbServiceCore.unregisterAdapterStateCallbacks(adapterStateCallbacks);
+    }
+
+    @Override
+    public long getTimestampResolutionNanos(String chipId) throws RemoteException {
+        enforceUwbPrivilegedPermission();
+        checkValidChipId(chipId);
+        return mUwbServiceCore.getTimestampResolutionNanos();
+    }
+
+    @Override
+    public PersistableBundle getSpecificationInfo(String chipId) throws RemoteException {
+        enforceUwbPrivilegedPermission();
+        checkValidChipId(chipId);
+        return mUwbServiceCore.getSpecificationInfo();
+    }
+
+    @Override
+    public void openRanging(AttributionSource attributionSource,
+            SessionHandle sessionHandle,
+            IUwbRangingCallbacks rangingCallbacks,
+            PersistableBundle parameters,
+            String chipId) throws RemoteException {
+
+        enforceUwbPrivilegedPermission();
+        mUwbInjector.enforceUwbRangingPermissionForPreflight(attributionSource);
+        mUwbServiceCore.openRanging(attributionSource, sessionHandle, rangingCallbacks, parameters);
+    }
+
+    @Override
+    public void startRanging(SessionHandle sessionHandle, PersistableBundle parameters)
+            throws RemoteException {
+        enforceUwbPrivilegedPermission();
+        mUwbServiceCore.startRanging(sessionHandle, parameters);
+    }
+
+    @Override
+    public void reconfigureRanging(SessionHandle sessionHandle, PersistableBundle parameters)
+            throws RemoteException {
+        enforceUwbPrivilegedPermission();
+        mUwbServiceCore.reconfigureRanging(sessionHandle, parameters);
+    }
+
+    @Override
+    public void stopRanging(SessionHandle sessionHandle) throws RemoteException {
+        enforceUwbPrivilegedPermission();
+        mUwbServiceCore.stopRanging(sessionHandle);
+    }
+
+    @Override
+    public void closeRanging(SessionHandle sessionHandle) throws RemoteException {
+        enforceUwbPrivilegedPermission();
+        mUwbServiceCore.closeRanging(sessionHandle);
+    }
+
+    @Override
+    public synchronized int sendVendorUciMessage(int gid, int oid, byte[] payload)
+            throws RemoteException {
+        enforceUwbPrivilegedPermission();
+        return mUwbServiceCore.sendVendorUciMessage(gid, oid, payload);
+    }
+
+    @Override
+    public void addControlee(SessionHandle sessionHandle, PersistableBundle params) {
+        enforceUwbPrivilegedPermission();
+        mUwbServiceCore.addControlee(sessionHandle, params);
+    }
+
+    @Override
+    public void removeControlee(SessionHandle sessionHandle, PersistableBundle params) {
+        enforceUwbPrivilegedPermission();
+        mUwbServiceCore.removeControlee(sessionHandle, params);
+    }
+
+    @Override
+    public void pause(SessionHandle sessionHandle, PersistableBundle params) {
+        enforceUwbPrivilegedPermission();
+        // TODO(b/200678461): Implement this.
+        throw new IllegalStateException("Not implemented");
+    }
+
+    @Override
+    public void resume(SessionHandle sessionHandle, PersistableBundle params) {
+        enforceUwbPrivilegedPermission();
+        // TODO(b/200678461): Implement this.
+        throw new IllegalStateException("Not implemented");
+    }
+
+    @Override
+    public void sendData(SessionHandle sessionHandle, UwbAddress remoteDeviceAddress,
+            PersistableBundle params, byte[] data) {
+        enforceUwbPrivilegedPermission();
+        // TODO(b/200678461): Implement this.
+        throw new IllegalStateException("Not implemented");
+    }
+
+    @Override
+    public synchronized int getAdapterState() throws RemoteException {
+        return mUwbServiceCore.getAdapterState();
+    }
+
+    @Override
+    public synchronized void setEnabled(boolean enabled) throws RemoteException {
+        enforceUwbPrivilegedPermission();
+        persistUwbToggleState(enabled);
+        // Shell command from rooted shell, we allow UWB toggle on even if APM mode is on.
+        if (Binder.getCallingUid() == Process.ROOT_UID) {
+            mUwbServiceCore.setEnabled(isUwbToggleEnabled());
+            return;
+        }
+        mUwbServiceCore.setEnabled(isUwbEnabled());
+    }
+
+    @Override
+    public List<PersistableBundle> getChipInfos() {
+        enforceUwbPrivilegedPermission();
+        List<ChipInfoParams> chipInfoParamsList = mUwbInjector.getMultichipData().getChipInfos();
+        List<PersistableBundle> chipInfos = new ArrayList<>();
+        for (ChipInfoParams chipInfoParams : chipInfoParamsList) {
+            chipInfos.add(chipInfoParams.toBundle());
+        }
+        return chipInfos;
+    }
+
+    @Override
+    public List<String> getChipIds() {
+        enforceUwbPrivilegedPermission();
+        List<ChipInfoParams> chipInfoParamsList = mUwbInjector.getMultichipData().getChipInfos();
+        List<String> chipIds = new ArrayList<>();
+        for (ChipInfoParams chipInfoParams : chipInfoParamsList) {
+            chipIds.add(chipInfoParams.getChipId());
+        }
+        return chipIds;
+    }
+
+    @Override
+    public String getDefaultChipId() {
+        enforceUwbPrivilegedPermission();
+        return mUwbInjector.getMultichipData().getDefaultChipId();
+    }
+
+    @Override
+    public PersistableBundle addServiceProfile(@NonNull PersistableBundle parameters) {
+        enforceUwbPrivilegedPermission();
+        // TODO(b/200678461): Implement this.
+        throw new IllegalStateException("Not implemented");
+    }
+
+    @Override
+    public int removeServiceProfile(@NonNull PersistableBundle parameters) {
+        enforceUwbPrivilegedPermission();
+        // TODO(b/200678461): Implement this.
+        throw new IllegalStateException("Not implemented");
+    }
+
+    @Override
+    public PersistableBundle getAllServiceProfiles() {
+        enforceUwbPrivilegedPermission();
+        // TODO(b/200678461): Implement this.
+        throw new IllegalStateException("Not implemented");
+    }
+
+    @NonNull
+    @Override
+    public PersistableBundle getAdfProvisioningAuthorities(@NonNull PersistableBundle parameters) {
+        enforceUwbPrivilegedPermission();
+        // TODO(b/200678461): Implement this.
+        throw new IllegalStateException("Not implemented");
+    }
+
+    @NonNull
+    @Override
+    public PersistableBundle getAdfCertificateAndInfo(@NonNull PersistableBundle parameters) {
+        enforceUwbPrivilegedPermission();
+        // TODO(b/200678461): Implement this.
+        throw new IllegalStateException("Not implemented");
+    }
+
+    @Override
+    public void provisionProfileAdfByScript(@NonNull PersistableBundle serviceProfileBundle,
+            @NonNull IUwbAdfProvisionStateCallbacks callback) {
+        enforceUwbPrivilegedPermission();
+        // TODO(b/200678461): Implement this.
+        throw new IllegalStateException("Not implemented");
+    }
+
+    @Override
+    public int removeProfileAdf(@NonNull PersistableBundle serviceProfileBundle) {
+        enforceUwbPrivilegedPermission();
+        // TODO(b/200678461): Implement this.
+        throw new IllegalStateException("Not implemented");
+    }
+
+    @Override
+    public int handleShellCommand(@NonNull ParcelFileDescriptor in,
+            @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
+            @NonNull String[] args) {
+
+        UwbShellCommand shellCommand =  mUwbInjector.makeUwbShellCommand(this);
+        return shellCommand.exec(this, in.getFileDescriptor(), out.getFileDescriptor(),
+                err.getFileDescriptor(), args);
+    }
+
+    private void persistUwbToggleState(boolean enabled) {
+        mUwbSettingsStore.put(UwbSettingsStore.SETTINGS_TOGGLE_STATE, enabled);
+    }
+
+    private boolean isUwbToggleEnabled() {
+        return mUwbSettingsStore.get(UwbSettingsStore.SETTINGS_TOGGLE_STATE);
+    }
+
+    /** Returns true if airplane mode is turned on. */
+    private boolean isAirplaneModeOn() {
+        return mUwbInjector.getSettingsInt(
+                Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
+    }
+
+    /** Returns true if UWB is enabled - based on UWB and APM toggle */
+    private boolean isUwbEnabled() {
+        return isUwbToggleEnabled() && !isAirplaneModeOn();
+    }
+
+    private void registerAirplaneModeReceiver() {
+        mContext.registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                handleAirplaneModeEvent();
+            }
+        }, new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
+    }
+
+    private void handleAirplaneModeEvent() {
+        try {
+            mUwbServiceCore.setEnabled(isUwbEnabled());
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to set UWB Adapter state.", e);
+        }
+    }
+
+    private void checkValidChipId(String chipId) {
+        if (chipId != null && !getChipIds().contains(chipId)) {
+            throw new IllegalArgumentException("invalid chipId: " + chipId);
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbSessionManager.java b/service/java/com/android/server/uwb/UwbSessionManager.java
new file mode 100644
index 0000000..76bae00
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbSessionManager.java
@@ -0,0 +1,1041 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb;
+
+import static com.android.server.uwb.data.UwbUciConstants.REASON_STATE_CHANGE_WITH_SESSION_MANAGEMENT_COMMANDS;
+
+import static com.google.uwb.support.fira.FiraParams.MULTICAST_LIST_UPDATE_ACTION_ADD;
+import static com.google.uwb.support.fira.FiraParams.MULTICAST_LIST_UPDATE_ACTION_DELETE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AlarmManager;
+import android.content.AttributionSource;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Pair;
+import android.uwb.IUwbAdapter;
+import android.uwb.IUwbRangingCallbacks;
+import android.uwb.RangingChangeReason;
+import android.uwb.SessionHandle;
+import android.uwb.UwbAddress;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.uwb.data.UwbMulticastListUpdateStatus;
+import com.android.server.uwb.data.UwbRangingData;
+import com.android.server.uwb.data.UwbTwoWayMeasurement;
+import com.android.server.uwb.data.UwbUciConstants;
+import com.android.server.uwb.jni.INativeUwbManager;
+import com.android.server.uwb.jni.NativeUwbManager;
+import com.android.server.uwb.proto.UwbStatsLog;
+import com.android.server.uwb.util.ArrayUtils;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.ccc.CccOpenRangingParams;
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.ccc.CccRangingStartedParams;
+import com.google.uwb.support.ccc.CccStartRangingParams;
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraRangingReconfigureParams;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class UwbSessionManager implements INativeUwbManager.SessionNotification {
+
+    private static final String TAG = "UwbSessionManager";
+    private static final int SESSION_OPEN_RANGING = 1;
+    private static final int SESSION_START_RANGING = 2;
+    private static final int SESSION_STOP_RANGING = 3;
+    private static final int SESSION_RECONFIG_RANGING = 4;
+    private static final int SESSION_CLOSE = 5;
+    private static final int SESSION_ON_DEINIT = 6;
+
+    // TODO: don't expose the internal field for testing.
+    @VisibleForTesting
+    final ConcurrentHashMap<Integer, UwbSession> mSessionTable = new ConcurrentHashMap();
+    private final NativeUwbManager mNativeUwbManager;
+    private final UwbMetrics mUwbMetrics;
+    private final UwbConfigurationManager mConfigurationManager;
+    private final UwbSessionNotificationManager mSessionNotificationManager;
+    private final UwbInjector mUwbInjector;
+    private final AlarmManager mAlarmManager;
+    private final int mMaxSessionNumber;
+    private final EventTask mEventTask;
+
+    public UwbSessionManager(UwbConfigurationManager uwbConfigurationManager,
+            NativeUwbManager nativeUwbManager, UwbMetrics uwbMetrics,
+            UwbSessionNotificationManager uwbSessionNotificationManager,
+            UwbInjector uwbInjector, AlarmManager alarmManager, Looper serviceLooper) {
+        mNativeUwbManager = nativeUwbManager;
+        mNativeUwbManager.setSessionListener(this);
+        mUwbMetrics = uwbMetrics;
+        mConfigurationManager = uwbConfigurationManager;
+        mSessionNotificationManager = uwbSessionNotificationManager;
+        mUwbInjector = uwbInjector;
+        mAlarmManager = alarmManager;
+        mMaxSessionNumber = mNativeUwbManager.getMaxSessionNumber();
+        mEventTask = new EventTask(serviceLooper);
+    }
+
+    private static boolean hasAllRangingResultError(@NonNull UwbRangingData rangingData) {
+        for (UwbTwoWayMeasurement measure : rangingData.getRangingTwoWayMeasures()) {
+            if (measure.getRangingStatus() == UwbUciConstants.STATUS_CODE_OK) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public void onRangeDataNotificationReceived(UwbRangingData rangingData) {
+        long sessionId = rangingData.getSessionId();
+        UwbSession uwbSession = getUwbSession((int) sessionId);
+        if (uwbSession != null) {
+            mUwbMetrics.logRangingResult(uwbSession.getProfileType(), rangingData);
+            mSessionNotificationManager.onRangingResult(uwbSession, rangingData);
+            if (hasAllRangingResultError(rangingData)) {
+                uwbSession.startRangingResultErrorStreakTimerIfNotSet();
+            } else {
+                uwbSession.stopRangingResultErrorStreakTimerIfSet();
+            }
+        } else {
+            Log.i(TAG, "Session is not initialized or Ranging Data is Null");
+        }
+    }
+
+    @Override
+    public void onMulticastListUpdateNotificationReceived(
+            UwbMulticastListUpdateStatus multicastListUpdateStatus) {
+        Log.d(TAG, "onMulticastListUpdateNotificationReceived");
+        UwbSession uwbSession = getUwbSession((int) multicastListUpdateStatus.getSessionId());
+        if (uwbSession == null) {
+            Log.d(TAG, "onMulticastListUpdateNotificationReceived - invalid session");
+            return;
+        }
+        uwbSession.setMulticastListUpdateStatus(multicastListUpdateStatus);
+        synchronized (uwbSession.getWaitObj()) {
+            uwbSession.getWaitObj().blockingNotify();
+        }
+    }
+
+    @Override
+    public void onSessionStatusNotificationReceived(long sessionId, int state, int reasonCode) {
+        Log.i(TAG, "onSessionStatusNotificationReceived - Session ID : " + sessionId + ", state : "
+                + UwbSessionNotificationHelper.getSessionStateString(state) + " reasonCode:"
+                + reasonCode);
+        UwbSession uwbSession = mSessionTable.get((int) sessionId);
+
+        if (uwbSession == null) {
+            Log.d(TAG, "onSessionStatusNotificationReceived - invalid session");
+            return;
+        }
+        int prevState = uwbSession.getSessionState();
+        synchronized (uwbSession.getWaitObj()) {
+            uwbSession.getWaitObj().blockingNotify();
+            setCurrentSessionState((int) sessionId, state);
+        }
+
+        //TODO : process only error handling in this switch function, b/218921154
+        switch (state) {
+            case UwbUciConstants.UWB_SESSION_STATE_IDLE:
+                if (prevState == UwbUciConstants.UWB_SESSION_STATE_ACTIVE) {
+                    // If session was stopped explicitly, then the onStopped() is sent from
+                    // stopRanging method.
+                    if (reasonCode != REASON_STATE_CHANGE_WITH_SESSION_MANAGEMENT_COMMANDS) {
+                        mSessionNotificationManager.onRangingStoppedWithUciReasonCode(
+                                uwbSession, reasonCode);
+                        mUwbMetrics.longRangingStopEvent(uwbSession);
+                    }
+                } else if (prevState == UwbUciConstants.UWB_SESSION_STATE_IDLE) {
+                    //mSessionNotificationManager.onRangingReconfigureFailed(
+                    //      uwbSession, reasonCode);
+                }
+                break;
+            case UwbUciConstants.UWB_SESSION_STATE_DEINIT:
+                mEventTask.execute(SESSION_ON_DEINIT, uwbSession);
+                break;
+            default:
+                break;
+        }
+    }
+
+    private byte getSessionType(String protocolName) {
+        byte sessionType = UwbUciConstants.SESSION_TYPE_RANGING;
+        if (protocolName.equals(FiraParams.PROTOCOL_NAME)) {
+            sessionType = UwbUciConstants.SESSION_TYPE_RANGING;
+        } else if (protocolName.equals(CccParams.PROTOCOL_NAME)) {
+            sessionType = UwbUciConstants.SESSION_TYPE_CCC;
+        }
+        return sessionType;
+    }
+
+    private int setAppConfigurations(UwbSession uwbSession) {
+        return mConfigurationManager.setAppConfigurations(uwbSession.getSessionId(),
+                uwbSession.getParams());
+    }
+
+    public synchronized void initSession(AttributionSource attributionSource,
+            SessionHandle sessionHandle, int sessionId,
+            String protocolName, Params params, IUwbRangingCallbacks rangingCallbacks)
+            throws RemoteException {
+        Log.i(TAG, "initSession() : Enter - sessionId : " + sessionId);
+        UwbSession uwbSession =  createUwbSession(attributionSource, sessionHandle, sessionId,
+                protocolName, params, rangingCallbacks);
+        if (isExistedSession(sessionId)) {
+            Log.i(TAG, "Duplicated sessionId");
+            rangingCallbacks.onRangingOpenFailed(sessionHandle, RangingChangeReason.BAD_PARAMETERS,
+                    UwbSessionNotificationHelper.convertUciStatusToParam(protocolName,
+                            UwbUciConstants.STATUS_CODE_ERROR_SESSION_DUPLICATE));
+            mUwbMetrics.logRangingInitEvent(uwbSession,
+                    UwbUciConstants.STATUS_CODE_ERROR_SESSION_DUPLICATE);
+            return;
+        }
+
+        if (getSessionCount() >= mMaxSessionNumber) {
+            Log.i(TAG, "Max Sessions Exceeded");
+            rangingCallbacks.onRangingOpenFailed(sessionHandle,
+                    RangingChangeReason.MAX_SESSIONS_REACHED,
+                    UwbSessionNotificationHelper.convertUciStatusToParam(protocolName,
+                            UwbUciConstants.STATUS_CODE_ERROR_MAX_SESSIONS_EXCEEDED));
+            mUwbMetrics.logRangingInitEvent(uwbSession,
+                    UwbUciConstants.STATUS_CODE_ERROR_MAX_SESSIONS_EXCEEDED);
+            return;
+        }
+
+        byte sessionType = getSessionType(protocolName);
+
+        try {
+            uwbSession.getBinder().linkToDeath(uwbSession, 0);
+        } catch (RemoteException e) {
+            uwbSession.binderDied();
+            Log.e(TAG, "linkToDeath fail - sessionID : " + uwbSession.getSessionId());
+            rangingCallbacks.onRangingOpenFailed(sessionHandle, RangingChangeReason.UNKNOWN,
+                    UwbSessionNotificationHelper.convertUciStatusToParam(protocolName,
+                            UwbUciConstants.STATUS_CODE_FAILED));
+            mUwbMetrics.logRangingInitEvent(uwbSession,
+                    UwbUciConstants.STATUS_CODE_FAILED);
+            removeSession(uwbSession);
+            return;
+        }
+
+        mSessionTable.put(sessionId, uwbSession);
+        mEventTask.execute(SESSION_OPEN_RANGING, uwbSession);
+        return;
+    }
+
+    // TODO: use UwbInjector.
+    @VisibleForTesting
+    UwbSession createUwbSession(AttributionSource attributionSource, SessionHandle sessionHandle,
+            int sessionId, String protocolName, Params params,
+            IUwbRangingCallbacks iUwbRangingCallbacks) {
+        return new UwbSession(attributionSource, sessionHandle, sessionId, protocolName, params,
+                iUwbRangingCallbacks);
+    }
+
+    public synchronized void deInitSession(SessionHandle sessionHandle) {
+        if (!isExistedSession(sessionHandle)) {
+            Log.i(TAG, "Not initialized session ID");
+            return;
+        }
+
+        int sessionId = getSessionId(sessionHandle);
+        Log.i(TAG, "sessionDeInit() - Session ID : " + sessionId);
+        UwbSession uwbSession = getUwbSession(sessionId);
+        mEventTask.execute(SESSION_CLOSE, uwbSession);
+        return;
+    }
+
+    public synchronized void startRanging(SessionHandle sessionHandle, @Nullable Params params) {
+        if (!isExistedSession(sessionHandle)) {
+            Log.i(TAG, "Not initialized session ID");
+            return;
+        }
+
+        int sessionId = getSessionId(sessionHandle);
+        Log.i(TAG, "startRanging() - Session ID : " + sessionId);
+
+        UwbSession uwbSession = getUwbSession(sessionId);
+
+        int currentSessionState = getCurrentSessionState(sessionId);
+        if (currentSessionState == UwbUciConstants.UWB_SESSION_STATE_IDLE) {
+            if (uwbSession.getProtocolName().equals(CccParams.PROTOCOL_NAME)
+                    && params instanceof CccStartRangingParams) {
+                CccStartRangingParams rangingStartParams = (CccStartRangingParams) params;
+                Log.i(TAG, "startRanging() - update RAN multiplier: "
+                        + rangingStartParams.getRanMultiplier());
+                // Need to update the RAN multiplier from the CccStartRangingParams for CCC session.
+                uwbSession.updateCccParamsOnStart(rangingStartParams);
+            }
+            mEventTask.execute(SESSION_START_RANGING, uwbSession);
+        } else if (currentSessionState == UwbUciConstants.UWB_SESSION_STATE_ACTIVE) {
+            Log.i(TAG, "session is already ranging");
+            mSessionNotificationManager.onRangingStartFailed(
+                    uwbSession, UwbUciConstants.STATUS_CODE_REJECTED);
+        } else {
+            Log.i(TAG, "session can't start ranging");
+            mSessionNotificationManager.onRangingStartFailed(
+                    uwbSession, UwbUciConstants.STATUS_CODE_FAILED);
+            mUwbMetrics.longRangingStartEvent(uwbSession, UwbUciConstants.STATUS_CODE_FAILED);
+        }
+    }
+
+    public synchronized void stopRanging(SessionHandle sessionHandle) {
+        if (!isExistedSession(sessionHandle)) {
+            Log.i(TAG, "Not initialized session ID");
+            return;
+        }
+
+        int sessionId = getSessionId(sessionHandle);
+        Log.i(TAG, "stopRanging() - Session ID : " + sessionId);
+
+        UwbSession uwbSession = getUwbSession(sessionId);
+        int currentSessionState = getCurrentSessionState(sessionId);
+        if (currentSessionState == UwbUciConstants.UWB_SESSION_STATE_ACTIVE) {
+            mEventTask.execute(SESSION_STOP_RANGING, uwbSession);
+        } else if (currentSessionState == UwbUciConstants.UWB_SESSION_STATE_IDLE) {
+            Log.i(TAG, "session is already idle state");
+            mSessionNotificationManager.onRangingStopped(uwbSession,
+                    UwbUciConstants.STATUS_CODE_OK);
+            mUwbMetrics.longRangingStopEvent(uwbSession);
+        } else {
+            mSessionNotificationManager.onRangingStopFailed(uwbSession,
+                    UwbUciConstants.STATUS_CODE_REJECTED);
+            Log.i(TAG, "Not active session ID");
+        }
+    }
+
+    public UwbSession getUwbSession(int sessionId) {
+        return mSessionTable.get(sessionId);
+    }
+
+    public Integer getSessionId(SessionHandle sessionHandle) {
+        for (Map.Entry<Integer, UwbSession> sessionEntry : mSessionTable.entrySet()) {
+            UwbSession uwbSession = sessionEntry.getValue();
+            if ((uwbSession.getSessionHandle()).equals(sessionHandle)) {
+                return sessionEntry.getKey();
+            }
+        }
+        return null;
+    }
+
+    private int getActiveSessionCount() {
+        int count = 0;
+        for (Map.Entry<Integer, UwbSession> sessionEntry : mSessionTable.entrySet()) {
+            UwbSession uwbSession = sessionEntry.getValue();
+            if ((uwbSession.getSessionState() == UwbUciConstants.DEVICE_STATE_ACTIVE)) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    public boolean isExistedSession(SessionHandle sessionHandle) {
+        return (getSessionId(sessionHandle) != null);
+    }
+
+    public boolean isExistedSession(int sessionId) {
+        return mSessionTable.containsKey(sessionId);
+    }
+
+    public void stopAllRanging() {
+        Log.d(TAG, "stopAllRanging()");
+        for (Map.Entry<Integer, UwbSession> sessionEntry : mSessionTable.entrySet()) {
+            int status = mNativeUwbManager.stopRanging(sessionEntry.getKey());
+
+            if (status != UwbUciConstants.STATUS_CODE_OK) {
+                Log.i(TAG, "stopAllRanging() - Session " + sessionEntry.getKey()
+                        + " is failed to stop ranging");
+            } else {
+                UwbSession uwbSession = sessionEntry.getValue();
+                mUwbMetrics.longRangingStopEvent(uwbSession);
+                uwbSession.setSessionState(UwbUciConstants.UWB_SESSION_STATE_IDLE);
+            }
+        }
+    }
+
+    public synchronized void deinitAllSession() {
+        Log.d(TAG, "deinitAllSession()");
+        for (Map.Entry<Integer, UwbSession> sessionEntry : mSessionTable.entrySet()) {
+            UwbSession uwbSession = sessionEntry.getValue();
+            onDeInit(uwbSession);
+        }
+
+        // Not resetting chip on UWB toggle off.
+        // mNativeUwbManager.resetDevice(UwbUciConstants.UWBS_RESET);
+    }
+
+    public synchronized void onDeInit(UwbSession uwbSession) {
+        if (!isExistedSession(uwbSession.getSessionId())) {
+            Log.i(TAG, "onDeinit - Ignoring already deleted session " + uwbSession.getSessionId());
+            return;
+        }
+        Log.d(TAG, "onDeinit: " + uwbSession.getSessionId());
+        mSessionNotificationManager.onRangingClosedWithApiReasonCode(uwbSession,
+                RangingChangeReason.SYSTEM_POLICY);
+        mUwbMetrics.logRangingCloseEvent(uwbSession, UwbUciConstants.STATUS_CODE_OK);
+        removeSession(uwbSession);
+    }
+
+    public void setCurrentSessionState(int sessionId, int state) {
+        UwbSession uwbSession = mSessionTable.get(sessionId);
+        if (uwbSession != null) {
+            uwbSession.setSessionState(state);
+        }
+    }
+
+    public int getCurrentSessionState(int sessionId) {
+        UwbSession uwbSession = mSessionTable.get(sessionId);
+        if (uwbSession != null) {
+            return uwbSession.getSessionState();
+        }
+        return UwbUciConstants.UWB_SESSION_STATE_ERROR;
+    }
+
+    public int getSessionCount() {
+        return mSessionTable.size();
+    }
+
+    public Set<Integer> getSessionIdSet() {
+        return mSessionTable.keySet();
+    }
+
+    public int reconfigure(SessionHandle sessionHandle, @Nullable Params params) {
+        Log.i(TAG, "reconfigure() - Session Handle : " + sessionHandle);
+        int status = UwbUciConstants.STATUS_CODE_ERROR_SESSION_NOT_EXIST;
+        if (!isExistedSession(sessionHandle)) {
+            Log.i(TAG, "Not initialized session ID");
+            return status;
+        }
+        Pair<SessionHandle, Params> info = new Pair<>(sessionHandle, params);
+        mEventTask.execute(SESSION_RECONFIG_RANGING, info);
+        return 0;
+    }
+
+    void removeSession(UwbSession uwbSession) {
+        if (uwbSession != null) {
+            uwbSession.getBinder().unlinkToDeath(uwbSession, 0);
+            mSessionTable.remove(uwbSession.getSessionId());
+        }
+    }
+
+    private class EventTask extends Handler {
+
+        EventTask(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            int type = msg.what;
+            switch (type) {
+                case SESSION_OPEN_RANGING: {
+                    UwbSession uwbSession = (UwbSession) msg.obj;
+                    openRanging(uwbSession);
+                    break;
+                }
+
+                case SESSION_START_RANGING: {
+                    UwbSession uwbSession = (UwbSession) msg.obj;
+                    startRanging(uwbSession);
+                    break;
+                }
+
+                case SESSION_STOP_RANGING: {
+                    UwbSession uwbSession = (UwbSession) msg.obj;
+                    stopRanging(uwbSession);
+                    break;
+                }
+
+                case SESSION_RECONFIG_RANGING: {
+                    Log.d(TAG, "SESSION_RECONFIG_RANGING");
+                    Pair<SessionHandle, Params> info = (Pair<SessionHandle, Params>) msg.obj;
+                    reconfigure(info.first, info.second);
+                    break;
+                }
+
+                case SESSION_CLOSE: {
+                    UwbSession uwbSession = (UwbSession) msg.obj;
+                    close(uwbSession);
+                    break;
+                }
+
+                case SESSION_ON_DEINIT : {
+                    UwbSession uwbSession = (UwbSession) msg.obj;
+                    onDeInit(uwbSession);
+                    break;
+                }
+
+                default: {
+                    Log.d(TAG, "EventTask : Undefined Task");
+                    break;
+                }
+            }
+        }
+
+        public void execute(int task, Object obj) {
+            Message msg = mEventTask.obtainMessage();
+            msg.what = task;
+            msg.obj = obj;
+            this.sendMessage(msg);
+        }
+
+        private void openRanging(UwbSession uwbSession) {
+            // TODO(b/211445008): Consolidate to a single uwb thread.
+            ExecutorService executor = Executors.newSingleThreadExecutor();
+            FutureTask<Integer> initSessionTask = new FutureTask<>(
+                    () -> {
+                        int status = UwbUciConstants.STATUS_CODE_FAILED;
+                        synchronized (uwbSession.getWaitObj()) {
+                            status = mNativeUwbManager.initSession(
+                                    uwbSession.getSessionId(),
+                                    getSessionType(uwbSession.getParams().getProtocolName()));
+                            if (status != UwbUciConstants.STATUS_CODE_OK) {
+                                return status;
+                            }
+
+                            uwbSession.getWaitObj().blockingWait();
+                            status = UwbUciConstants.STATUS_CODE_FAILED;
+                            if (uwbSession.getSessionState()
+                                    == UwbUciConstants.UWB_SESSION_STATE_INIT) {
+                                status = UwbSessionManager.this.setAppConfigurations(uwbSession);
+                                if (status != UwbUciConstants.STATUS_CODE_OK) {
+                                    return status;
+                                }
+
+                                uwbSession.getWaitObj().blockingWait();
+                                status = UwbUciConstants.STATUS_CODE_FAILED;
+                                if (uwbSession.getSessionState()
+                                        == UwbUciConstants.UWB_SESSION_STATE_IDLE) {
+                                    mSessionNotificationManager.onRangingOpened(uwbSession);
+                                    status = UwbUciConstants.STATUS_CODE_OK;
+                                } else {
+                                    status = UwbUciConstants.STATUS_CODE_FAILED;
+                                }
+                                return status;
+                            }
+                            return status;
+                        }
+                    });
+            executor.submit(initSessionTask);
+
+            int status = UwbUciConstants.STATUS_CODE_FAILED;
+            try {
+                status = initSessionTask.get(
+                        IUwbAdapter.RANGING_SESSION_OPEN_THRESHOLD_MS, TimeUnit.MILLISECONDS);
+            } catch (TimeoutException e) {
+                executor.shutdownNow();
+                Log.i(TAG, "Failed to initialize session - status : TIMEOUT");
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            } catch (ExecutionException e) {
+                e.printStackTrace();
+            }
+
+            mUwbMetrics.logRangingInitEvent(uwbSession, status);
+            if (status != UwbUciConstants.STATUS_CODE_OK) {
+                Log.i(TAG, "Failed to initialize session - status : " + status);
+                mSessionNotificationManager.onRangingOpenFailed(uwbSession, status);
+                mNativeUwbManager.deInitSession(uwbSession.getSessionId());
+                removeSession(uwbSession);
+            }
+            Log.i(TAG, "sessionInit() : finish - sessionId : " + uwbSession.getSessionId());
+        }
+
+        private void startRanging(UwbSession uwbSession) {
+            // TODO(b/211445008): Consolidate to a single uwb thread.
+            ExecutorService executor = Executors.newSingleThreadExecutor();
+            FutureTask<Integer> startRangingTask = new FutureTask<>(
+                    () -> {
+                        int status = UwbUciConstants.STATUS_CODE_FAILED;
+                        synchronized (uwbSession.getWaitObj()) {
+                            if (uwbSession.getParams().getProtocolName()
+                                    .equals(CccParams.PROTOCOL_NAME)) {
+                                status = mConfigurationManager.setAppConfigurations(
+                                        uwbSession.getSessionId(),
+                                        uwbSession.getParams());
+                                if (status != UwbUciConstants.STATUS_CODE_OK) {
+                                    mSessionNotificationManager.onRangingStartFailed(
+                                            uwbSession, status);
+                                    return status;
+                                }
+                            }
+
+                            status = mNativeUwbManager.startRanging(uwbSession.getSessionId());
+                            if (status != UwbUciConstants.STATUS_CODE_OK) {
+                                mSessionNotificationManager.onRangingStartFailed(
+                                        uwbSession, status);
+                                return status;
+                            }
+                            uwbSession.getWaitObj().blockingWait();
+                            if (uwbSession.getSessionState()
+                                    == UwbUciConstants.UWB_SESSION_STATE_ACTIVE) {
+                                // TODO: Ensure |rangingStartedParams| is valid for FIRA sessions
+                                // as well.
+                                Params rangingStartedParams = uwbSession.getParams();
+                                // For CCC sessions, retrieve the app configs
+                                if (uwbSession.getProtocolName().equals(CccParams.PROTOCOL_NAME)) {
+                                    Pair<Integer, CccRangingStartedParams> statusAndParams  =
+                                            mConfigurationManager.getAppConfigurations(
+                                                    uwbSession.getSessionId(),
+                                                    CccParams.PROTOCOL_NAME,
+                                                    new byte[0],
+                                                    CccRangingStartedParams.class);
+                                    if (statusAndParams.first != UwbUciConstants.STATUS_CODE_OK) {
+                                        Log.e(TAG, "Failed to get CCC ranging started params");
+                                    }
+                                    rangingStartedParams = statusAndParams.second;
+                                }
+                                mSessionNotificationManager.onRangingStarted(
+                                        uwbSession, rangingStartedParams);
+                            } else {
+                                status = UwbUciConstants.STATUS_CODE_FAILED;
+                                mSessionNotificationManager.onRangingStartFailed(uwbSession,
+                                        status);
+                            }
+                        }
+                        return status;
+                    });
+
+            executor.submit(startRangingTask);
+
+            int status = UwbUciConstants.STATUS_CODE_FAILED;
+            try {
+                status = startRangingTask.get(
+                        IUwbAdapter.RANGING_SESSION_START_THRESHOLD_MS, TimeUnit.MILLISECONDS);
+            } catch (TimeoutException e) {
+                Log.i(TAG, "Failed to Start Ranging - status : TIMEOUT");
+                executor.shutdownNow();
+                mSessionNotificationManager.onRangingStartFailed(
+                        uwbSession, UwbUciConstants.STATUS_CODE_FAILED);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            } catch (ExecutionException e) {
+                e.printStackTrace();
+            }
+            mUwbMetrics.longRangingStartEvent(uwbSession, status);
+        }
+
+        private void stopRanging(UwbSession uwbSession) {
+            // TODO(b/211445008): Consolidate to a single uwb thread.
+            ExecutorService executor = Executors.newSingleThreadExecutor();
+            FutureTask<Integer> stopRangingTask = new FutureTask<>(
+                    () -> {
+                        int status = UwbUciConstants.STATUS_CODE_FAILED;
+                        synchronized (uwbSession.getWaitObj()) {
+                            status = mNativeUwbManager.stopRanging(uwbSession.getSessionId());
+                            if (status != UwbUciConstants.STATUS_CODE_OK) {
+                                mSessionNotificationManager.onRangingStopFailed(uwbSession, status);
+                                return status;
+                            }
+                            uwbSession.getWaitObj().blockingWait();
+                            if (uwbSession.getSessionState()
+                                    == UwbUciConstants.UWB_SESSION_STATE_IDLE) {
+                                mSessionNotificationManager.onRangingStopped(uwbSession, status);
+                            } else {
+                                status = UwbUciConstants.STATUS_CODE_FAILED;
+                                mSessionNotificationManager.onRangingStopFailed(uwbSession,
+                                        status);
+                            }
+                        }
+                        return status;
+                    });
+
+            executor.submit(stopRangingTask);
+
+            int status = UwbUciConstants.STATUS_CODE_FAILED;
+            try {
+                status = stopRangingTask.get(
+                        IUwbAdapter.RANGING_SESSION_START_THRESHOLD_MS, TimeUnit.MILLISECONDS);
+            } catch (TimeoutException e) {
+                Log.i(TAG, "Failed to Stop Ranging - status : TIMEOUT");
+                executor.shutdownNow();
+                mSessionNotificationManager.onRangingStopFailed(
+                        uwbSession, UwbUciConstants.STATUS_CODE_FAILED);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            } catch (ExecutionException e) {
+                e.printStackTrace();
+            }
+            if (status != UwbUciConstants.STATUS_CODE_FAILED) {
+                mUwbMetrics.longRangingStopEvent(uwbSession);
+            }
+            // Reset any stored error streak timestamp when session is stopped.
+            uwbSession.stopRangingResultErrorStreakTimerIfSet();
+        }
+
+        private void reconfigure(SessionHandle sessionHandle, @Nullable Params param) {
+            UwbSession uwbSession = getUwbSession(getSessionId(sessionHandle));
+            if (!(param instanceof FiraRangingReconfigureParams)) {
+                Log.e(TAG, "Invalid reconfigure params: " + param);
+                mSessionNotificationManager.onRangingReconfigureFailed(
+                        uwbSession, UwbUciConstants.STATUS_CODE_INVALID_PARAM);
+                return;
+            }
+            FiraRangingReconfigureParams rangingReconfigureParams =
+                    (FiraRangingReconfigureParams) param;
+            // TODO(b/211445008): Consolidate to a single uwb thread.
+            ExecutorService executor = Executors.newSingleThreadExecutor();
+            FutureTask<Integer> cmdTask = new FutureTask<>(
+                    () -> {
+                        int status = UwbUciConstants.STATUS_CODE_FAILED;
+                        synchronized (uwbSession.getWaitObj()) {
+                            // Handle SESSION_UPDATE_CONTROLLER_MULTICAST_LIST_CMD
+                            if (rangingReconfigureParams.getAction() != null) {
+                                Log.d(TAG, "call multicastlist update");
+                                int dstAddressListSize =
+                                        rangingReconfigureParams.getAddressList().length;
+                                List<Short> dstAddressList = new ArrayList<>();
+                                for (UwbAddress address :
+                                        rangingReconfigureParams.getAddressList()) {
+                                    dstAddressList.add(
+                                            ByteBuffer.wrap(address.toBytes()).getShort(0));
+                                }
+                                int[] subSessionIdList = null;
+                                if (!ArrayUtils.isEmpty(
+                                        rangingReconfigureParams.getSubSessionIdList())) {
+                                    subSessionIdList =
+                                        rangingReconfigureParams.getSubSessionIdList();
+                                } else {
+                                    // Set to 0's for the UCI stack.
+                                    subSessionIdList = new int[dstAddressListSize];
+                                }
+
+                                status = mNativeUwbManager.controllerMulticastListUpdate(
+                                        uwbSession.getSessionId(),
+                                        rangingReconfigureParams.getAction(),
+                                        subSessionIdList.length,
+                                        ArrayUtils.toPrimitive(dstAddressList),
+                                        subSessionIdList);
+                                if (status != UwbUciConstants.STATUS_CODE_OK) {
+                                    if (rangingReconfigureParams.getAction()
+                                            == MULTICAST_LIST_UPDATE_ACTION_ADD) {
+                                        mSessionNotificationManager.onControleeAddFailed(
+                                                uwbSession, status);
+                                    } else if (rangingReconfigureParams.getAction()
+                                            == MULTICAST_LIST_UPDATE_ACTION_DELETE) {
+                                        mSessionNotificationManager.onControleeRemoveFailed(
+                                                uwbSession, status);
+                                    }
+                                    return status;
+                                }
+
+                                uwbSession.getWaitObj().blockingWait();
+
+                                UwbMulticastListUpdateStatus multicastList =
+                                        uwbSession.getMulticastListUpdateStatus();
+                                if (multicastList != null) {
+                                    if (rangingReconfigureParams.getAction()
+                                            == MULTICAST_LIST_UPDATE_ACTION_ADD) {
+                                        for (int i = 0; i < multicastList.getNumOfControlee();
+                                                i++) {
+                                            if (multicastList.getStatus()[i]
+                                                    != UwbUciConstants.STATUS_CODE_OK) {
+                                                status = UwbUciConstants.STATUS_CODE_FAILED;
+                                                break;
+                                            }
+                                        }
+                                    }
+                                }
+                                if (status != UwbUciConstants.STATUS_CODE_OK) {
+                                    if (rangingReconfigureParams.getAction()
+                                            == MULTICAST_LIST_UPDATE_ACTION_ADD) {
+                                        mSessionNotificationManager.onControleeAddFailed(
+                                                uwbSession, status);
+                                    } else if (rangingReconfigureParams.getAction()
+                                            == MULTICAST_LIST_UPDATE_ACTION_DELETE) {
+                                        mSessionNotificationManager.onControleeRemoveFailed(
+                                                uwbSession, status);
+                                    }
+                                    return status;
+                                }
+                                if (rangingReconfigureParams.getAction()
+                                        == MULTICAST_LIST_UPDATE_ACTION_ADD) {
+                                    mSessionNotificationManager.onControleeAdded(uwbSession);
+                                } else if (rangingReconfigureParams.getAction()
+                                        == MULTICAST_LIST_UPDATE_ACTION_DELETE) {
+                                    mSessionNotificationManager.onControleeRemoved(uwbSession);
+                                }
+                            }
+                            status = mConfigurationManager.setAppConfigurations(
+                                    uwbSession.getSessionId(), param);
+                            Log.d(TAG, "status: " + status);
+                            if (status != UwbUciConstants.STATUS_CODE_OK) {
+                                return status;
+                            }
+                            mSessionNotificationManager.onRangingReconfigured(uwbSession);
+                            return status;
+                        }
+                    });
+
+            executor.submit(cmdTask);
+
+            int status = UwbUciConstants.STATUS_CODE_FAILED;
+            try {
+                status = cmdTask.get(
+                        IUwbAdapter.RANGING_SESSION_OPEN_THRESHOLD_MS, TimeUnit.MILLISECONDS);
+            } catch (TimeoutException e) {
+                Log.i(TAG, "Failed to Reconfigure - status : TIMEOUT");
+                executor.shutdownNow();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            } catch (ExecutionException e) {
+                e.printStackTrace();
+            }
+            if (status != UwbUciConstants.STATUS_CODE_OK) {
+                Log.i(TAG, "Failed to Reconfigure : " + status);
+                mSessionNotificationManager.onRangingReconfigureFailed(uwbSession, status);
+            }
+        }
+
+        private void close(UwbSession uwbSession) {
+            // TODO(b/211445008): Consolidate to a single uwb thread.
+            ExecutorService executor = Executors.newSingleThreadExecutor();
+            FutureTask<Integer> closeTask = new FutureTask<>(
+                    (Callable<Integer>) () -> {
+                        int status = UwbUciConstants.STATUS_CODE_FAILED;
+                        synchronized (uwbSession.getWaitObj()) {
+                            status = mNativeUwbManager.deInitSession(uwbSession.getSessionId());
+                            if (status != UwbUciConstants.STATUS_CODE_OK) {
+                                mSessionNotificationManager.onRangingClosed(uwbSession, status);
+                                return status;
+                            }
+                            uwbSession.getWaitObj().blockingWait();
+                            Log.i(TAG, "onRangingClosed - status : " + status);
+                            mSessionNotificationManager.onRangingClosed(uwbSession, status);
+                        }
+                        return status;
+                    });
+            executor.submit(closeTask);
+
+            int status = UwbUciConstants.STATUS_CODE_FAILED;
+            try {
+                status = closeTask.get(
+                        IUwbAdapter.RANGING_SESSION_CLOSE_THRESHOLD_MS, TimeUnit.MILLISECONDS);
+            } catch (TimeoutException e) {
+                Log.i(TAG, "Failed to Stop Ranging - status : TIMEOUT");
+                executor.shutdownNow();
+                mSessionNotificationManager.onRangingClosed(uwbSession, status);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            } catch (ExecutionException e) {
+                e.printStackTrace();
+            }
+            mUwbMetrics.logRangingCloseEvent(uwbSession, status);
+            removeSession(uwbSession);
+            Log.i(TAG, "deinit finish : status :" + status);
+        }
+    }
+
+    public class UwbSession implements IBinder.DeathRecipient {
+        // Amount of time we allow continuous failures before stopping the session.
+        @VisibleForTesting
+        public static final long RANGING_RESULT_ERROR_STREAK_TIMER_TIMEOUT_MS = 30_000L;
+        private static final String RANGING_RESULT_ERROR_STREAK_TIMER_TAG =
+                "UwbSessionRangingResultError";
+
+        private final AttributionSource mAttributionSource;
+        private final SessionHandle mSessionHandle;
+        private final int mSessionId;
+        private final IUwbRangingCallbacks mIUwbRangingCallbacks;
+        private final String mProtocolName;
+        private final IBinder mIBinder;
+        private final WaitObj mWaitObj;
+        public boolean isWait;
+        private Params mParams;
+        private int mSessionState;
+        private UwbMulticastListUpdateStatus mMulticastListUpdateStatus;
+        private final int mProfileType;
+        private AlarmManager.OnAlarmListener mRangingResultErrorStreakTimerListener;
+
+        UwbSession(AttributionSource attributionSource, SessionHandle sessionHandle, int sessionId,
+                String protocolName, Params params, IUwbRangingCallbacks iUwbRangingCallbacks) {
+            this.mAttributionSource = attributionSource;
+            this.mSessionHandle = sessionHandle;
+            this.mSessionId = sessionId;
+            this.mProtocolName = protocolName;
+            this.mIUwbRangingCallbacks = iUwbRangingCallbacks;
+            this.mIBinder = iUwbRangingCallbacks.asBinder();
+            this.mSessionState = UwbUciConstants.UWB_SESSION_STATE_DEINIT;
+            this.mParams = params;
+            this.mWaitObj = new WaitObj();
+            this.isWait = false;
+            this.mProfileType = convertProtolNameToProfileType(protocolName);
+        }
+
+        public AttributionSource getAttributionSource() {
+            return this.mAttributionSource;
+        }
+
+        public int getSessionId() {
+            return this.mSessionId;
+        }
+
+        public SessionHandle getSessionHandle() {
+            return this.mSessionHandle;
+        }
+
+        public Params getParams() {
+            return this.mParams;
+        }
+
+        public void updateCccParamsOnStart(CccStartRangingParams rangingStartParams) {
+            // Need to update the RAN multiplier from the CccStartRangingParams for CCC session.
+            CccOpenRangingParams rangingOpenedParams = (CccOpenRangingParams) mParams;
+            CccOpenRangingParams newParams =
+                    new CccOpenRangingParams.Builder()
+                            .setProtocolVersion(rangingOpenedParams.getProtocolVersion())
+                            .setUwbConfig(rangingOpenedParams.getUwbConfig())
+                            .setPulseShapeCombo(rangingOpenedParams.getPulseShapeCombo())
+                            .setSessionId(rangingOpenedParams.getSessionId())
+                            .setRanMultiplier(rangingStartParams.getRanMultiplier())
+                            .setChannel(rangingOpenedParams.getChannel())
+                            .setNumChapsPerSlot(rangingOpenedParams.getNumChapsPerSlot())
+                            .setNumResponderNodes(rangingOpenedParams.getNumResponderNodes())
+                            .setNumSlotsPerRound(rangingOpenedParams.getNumSlotsPerRound())
+                            .setSyncCodeIndex(rangingOpenedParams.getSyncCodeIndex())
+                            .setHoppingConfigMode(rangingOpenedParams.getHoppingConfigMode())
+                            .setHoppingSequence(rangingOpenedParams.getHoppingSequence())
+                            .build();
+            this.mParams = newParams;
+        }
+
+        public String getProtocolName() {
+            return this.mProtocolName;
+        }
+
+        public IUwbRangingCallbacks getIUwbRangingCallbacks() {
+            return this.mIUwbRangingCallbacks;
+        }
+
+        public int getSessionState() {
+            return this.mSessionState;
+        }
+
+        public void setSessionState(int state) {
+            this.mSessionState = state;
+        }
+
+        public void setMulticastListUpdateStatus(
+                UwbMulticastListUpdateStatus multicastListUpdateStatus) {
+            mMulticastListUpdateStatus = multicastListUpdateStatus;
+        }
+
+        public UwbMulticastListUpdateStatus getMulticastListUpdateStatus() {
+            return mMulticastListUpdateStatus;
+        }
+
+        private int convertProtolNameToProfileType(String protocolName) {
+            if (protocolName.equals(FiraParams.PROTOCOL_NAME)) {
+                return UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__FIRA;
+            } else if (protocolName.equals(CccParams.PROTOCOL_NAME)) {
+                return UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__CCC;
+            } else {
+                return UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__CUSTOMIZED;
+            }
+        }
+
+        public int getProfileType() {
+            return mProfileType;
+        }
+
+        public IBinder getBinder() {
+            return mIBinder;
+        }
+
+        public WaitObj getWaitObj() {
+            return mWaitObj;
+        }
+
+        /**
+         * Starts a timer to detect if the error streak is longer than
+         * {@link #RANGING_RESULT_ERROR_STREAK_TIMER_TIMEOUT_MS}.
+         */
+        public void startRangingResultErrorStreakTimerIfNotSet() {
+            // Start a timer on first failure to detect continuous failures.
+            if (mRangingResultErrorStreakTimerListener == null) {
+                mRangingResultErrorStreakTimerListener = () -> {
+                    Log.w(TAG, "Continuous errors or no ranging results detected for 30 seconds."
+                            + " Stopping session");
+                    stopRanging(mSessionHandle);
+                };
+                mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                        mUwbInjector.getElapsedSinceBootMillis()
+                                + RANGING_RESULT_ERROR_STREAK_TIMER_TIMEOUT_MS,
+                        RANGING_RESULT_ERROR_STREAK_TIMER_TAG,
+                        mRangingResultErrorStreakTimerListener, mEventTask);
+            }
+        }
+
+        public void stopRangingResultErrorStreakTimerIfSet() {
+            // Cancel error streak timer on any success.
+            if (mRangingResultErrorStreakTimerListener != null) {
+                mAlarmManager.cancel(mRangingResultErrorStreakTimerListener);
+                mRangingResultErrorStreakTimerListener = null;
+            }
+        }
+
+        @Override
+        public void binderDied() {
+            Log.i(TAG, "binderDied : getSessionId is getSessionId() " + getSessionId());
+
+            synchronized (UwbSessionManager.this) {
+                int status = mNativeUwbManager.deInitSession(getSessionId());
+                mUwbMetrics.logRangingCloseEvent(this, status);
+                if (status == UwbUciConstants.STATUS_CODE_OK) {
+                    removeSession(this);
+                    Log.i(TAG, "binderDied : Session count currently is " + getSessionCount());
+                } else {
+                    Log.e(TAG,
+                            "binderDied : sessionDeinit Failure because of NativeSessionDeinit "
+                                    + "Error");
+                }
+            }
+        }
+    }
+
+    // TODO: refactor the async operation flow.
+    // Wrapper for unit test.
+    @VisibleForTesting
+    static class WaitObj {
+        WaitObj() {
+        }
+
+        void blockingWait() throws InterruptedException {
+            wait();
+        }
+
+        void blockingNotify() {
+            notify();
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbSessionNotificationHelper.java b/service/java/com/android/server/uwb/UwbSessionNotificationHelper.java
new file mode 100644
index 0000000..06fcbc1
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbSessionNotificationHelper.java
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb;
+
+import android.os.PersistableBundle;
+import android.uwb.RangingChangeReason;
+
+import com.android.server.uwb.data.UwbUciConstants;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.ccc.CccRangingError;
+import com.google.uwb.support.fira.FiraStatusCode;
+
+public class UwbSessionNotificationHelper {
+    public static int convertUciReasonCodeToApiReasonCode(int reasonCode) {
+        /* set default */
+        int rangingChangeReason = RangingChangeReason.UNKNOWN;
+        switch (reasonCode) {
+            case UwbUciConstants.REASON_STATE_CHANGE_WITH_SESSION_MANAGEMENT_COMMANDS:
+                rangingChangeReason = RangingChangeReason.LOCAL_API;
+                break;
+            case UwbUciConstants.REASON_MAX_RANGING_ROUND_RETRY_COUNT_REACHED:
+                rangingChangeReason = RangingChangeReason.MAX_RR_RETRY_REACHED;
+                break;
+            case UwbUciConstants.REASON_MAX_NUMBER_OF_MEASUREMENTS_REACHED:
+                rangingChangeReason = RangingChangeReason.REMOTE_REQUEST;
+                break;
+            case UwbUciConstants.REASON_ERROR_INSUFFICIENT_SLOTS_PER_RR:
+            case UwbUciConstants.REASON_ERROR_SLOT_LENGTH_NOT_SUPPORTED:
+            case UwbUciConstants.REASON_ERROR_MAC_ADDRESS_MODE_NOT_SUPPORTED:
+            case UwbUciConstants.REASON_ERROR_INVALID_RANGING_INTERVAL:
+            case UwbUciConstants.REASON_ERROR_INVALID_STS_CONFIG:
+            case UwbUciConstants.REASON_ERROR_INVALID_RFRAME_CONFIG:
+                rangingChangeReason = RangingChangeReason.BAD_PARAMETERS;
+                break;
+        }
+        return rangingChangeReason;
+    }
+
+    public static int convertUciStatusToApiReasonCode(int status) {
+        /* set default */
+        int rangingChangeReason = RangingChangeReason.UNKNOWN;
+        switch (status) {
+            case UwbUciConstants.STATUS_CODE_OK:
+                rangingChangeReason = RangingChangeReason.LOCAL_API;
+                break;
+            case UwbUciConstants.STATUS_CODE_ERROR_MAX_SESSIONS_EXCEEDED:
+                rangingChangeReason = RangingChangeReason.MAX_SESSIONS_REACHED;
+                break;
+            case UwbUciConstants.STATUS_CODE_INVALID_PARAM:
+            case UwbUciConstants.STATUS_CODE_INVALID_RANGE:
+            case UwbUciConstants.STATUS_CODE_INVALID_MESSAGE_SIZE:
+                rangingChangeReason = RangingChangeReason.BAD_PARAMETERS;
+                break;
+            case UwbUciConstants.STATUS_CODE_ERROR_SESSION_NOT_EXIST:
+            case UwbUciConstants.STATUS_CODE_CCC_LIFECYCLE:
+            case UwbUciConstants.STATUS_CODE_CCC_SE_BUSY:
+                rangingChangeReason = RangingChangeReason.PROTOCOL_SPECIFIC;
+                break;
+        }
+        return rangingChangeReason;
+    }
+
+    private static @CccParams.ProtocolError int convertUciStatusToApiCccProtocolError(int status) {
+        switch (status) {
+            case UwbUciConstants.STATUS_CODE_ERROR_SESSION_NOT_EXIST:
+                return CccParams.PROTOCOL_ERROR_NOT_FOUND;
+            case UwbUciConstants.STATUS_CODE_CCC_LIFECYCLE:
+                return CccParams.PROTOCOL_ERROR_LIFECYCLE;
+            case UwbUciConstants.STATUS_CODE_CCC_SE_BUSY:
+                return CccParams.PROTOCOL_ERROR_SE_BUSY;
+            default:
+                return CccParams.PROTOCOL_ERROR_UNKNOWN;
+        }
+    }
+
+    public static PersistableBundle convertUciStatusToParam(String protocolName, int status) {
+        Params c;
+        if (protocolName.equals(CccParams.PROTOCOL_NAME)) {
+            c = new CccRangingError.Builder()
+                    .setError(convertUciStatusToApiCccProtocolError(status))
+                    .build();
+        } else {
+            c = new FiraStatusCode.Builder().setStatusCode(status).build();
+        }
+        return c.toBundle();
+    }
+
+    static String getSessionStateString(int state) {
+        String ret = "";
+        switch (state) {
+            case UwbUciConstants.UWB_SESSION_STATE_INIT:
+                ret = "INIT";
+                break;
+            case UwbUciConstants.UWB_SESSION_STATE_DEINIT:
+                ret = "DEINIT";
+                break;
+            case UwbUciConstants.UWB_SESSION_STATE_ACTIVE:
+                ret = "ACTIVE";
+                break;
+            case UwbUciConstants.UWB_SESSION_STATE_IDLE:
+                ret = "IDLE";
+                break;
+            case UwbUciConstants.UWB_SESSION_STATE_ERROR:
+                ret = "ERROR";
+                break;
+        }
+        return ret;
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbSessionNotificationManager.java b/service/java/com/android/server/uwb/UwbSessionNotificationManager.java
new file mode 100644
index 0000000..8230611
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbSessionNotificationManager.java
@@ -0,0 +1,421 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb;
+
+import android.annotation.NonNull;
+import android.os.PersistableBundle;
+import android.util.Log;
+import android.uwb.AngleMeasurement;
+import android.uwb.AngleOfArrivalMeasurement;
+import android.uwb.DistanceMeasurement;
+import android.uwb.IUwbRangingCallbacks;
+import android.uwb.RangingChangeReason;
+import android.uwb.RangingMeasurement;
+import android.uwb.RangingReport;
+import android.uwb.SessionHandle;
+import android.uwb.UwbAddress;
+
+import com.android.server.uwb.UwbSessionManager.UwbSession;
+import com.android.server.uwb.data.UwbRangingData;
+import com.android.server.uwb.data.UwbTwoWayMeasurement;
+import com.android.server.uwb.data.UwbUciConstants;
+import com.android.server.uwb.params.TlvUtil;
+import com.android.server.uwb.util.UwbUtil;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.ccc.CccRangingReconfiguredParams;
+import com.google.uwb.support.fira.FiraOpenSessionParams;
+import com.google.uwb.support.fira.FiraParams;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class UwbSessionNotificationManager {
+    private static final String TAG = "UwbSessionNotiManager";
+    private final UwbInjector mUwbInjector;
+
+    public UwbSessionNotificationManager(@NonNull UwbInjector uwbInjector) {
+        mUwbInjector = uwbInjector;
+    }
+
+    public void onRangingResult(UwbSession uwbSession, UwbRangingData rangingData) {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        boolean permissionGranted = mUwbInjector.checkUwbRangingPermissionForDataDelivery(
+                uwbSession.getAttributionSource(), "uwb ranging result");
+        if (!permissionGranted) {
+            Log.e(TAG, "Not delivering ranging result because of permission denial"
+                    + sessionHandle);
+            return;
+        }
+        try {
+            uwbRangingCallbacks.onRangingResult(
+                    sessionHandle,
+                    getRangingReport(rangingData, uwbSession.getProtocolName(),
+                            uwbSession.getParams(), mUwbInjector.getElapsedSinceBootNanos()));
+            Log.i(TAG, "IUwbRangingCallbacks - onRangingResult");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onRangingResult : Failed");
+            e.printStackTrace();
+        }
+    }
+
+    public void onRangingOpened(UwbSession uwbSession) {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        try {
+            uwbRangingCallbacks.onRangingOpened(sessionHandle);
+            Log.i(TAG, "IUwbRangingCallbacks - onRangingOpened");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onRangingOpened : Failed");
+            e.printStackTrace();
+        }
+    }
+
+    public void onRangingOpenFailed(UwbSession uwbSession, int status) {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+
+        try {
+            uwbRangingCallbacks.onRangingOpenFailed(sessionHandle,
+                    UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(status),
+                    UwbSessionNotificationHelper.convertUciStatusToParam(
+                            uwbSession.getProtocolName(), status));
+            Log.i(TAG, "IUwbRangingCallbacks - onRangingOpenFailed");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onRangingOpenFailed : Failed");
+            e.printStackTrace();
+        }
+    }
+
+    public void onRangingStarted(UwbSession uwbSession, Params rangingStartedParams) {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        try {
+            uwbRangingCallbacks.onRangingStarted(sessionHandle, rangingStartedParams.toBundle());
+            Log.i(TAG, "IUwbRangingCallbacks - onRangingStarted");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onRangingStarted : Failed");
+            e.printStackTrace();
+        }
+    }
+
+
+    public void onRangingStartFailed(UwbSession uwbSession, int status) {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        try {
+            uwbRangingCallbacks.onRangingStartFailed(sessionHandle,
+                    UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(status),
+                    UwbSessionNotificationHelper.convertUciStatusToParam(
+                            uwbSession.getProtocolName(), status));
+            Log.i(TAG, "IUwbRangingCallbacks - onRangingStartFailed");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onRangingStartFailed : Failed");
+            e.printStackTrace();
+        }
+    }
+
+    public void onRangingStoppedWithUciReasonCode(UwbSession uwbSession, int reasonCode)  {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        try {
+            uwbRangingCallbacks.onRangingStopped(sessionHandle,
+                    UwbSessionNotificationHelper.convertUciReasonCodeToApiReasonCode(reasonCode),
+                    new PersistableBundle());
+            Log.i(TAG, "IUwbRangingCallbacks - onRangingStopped");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onRangingStopped : Failed");
+            e.printStackTrace();
+        }
+    }
+
+    public void onRangingStopped(UwbSession uwbSession, int status)  {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        try {
+            uwbRangingCallbacks.onRangingStopped(sessionHandle,
+                    UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(
+                            status),
+                    UwbSessionNotificationHelper.convertUciStatusToParam(
+                            uwbSession.getProtocolName(), status));
+            Log.i(TAG, "IUwbRangingCallbacks - onRangingStopped");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onRangingStopped : Failed");
+            e.printStackTrace();
+        }
+    }
+
+    public void onRangingStopFailed(UwbSession uwbSession, int status) {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        try {
+            uwbRangingCallbacks.onRangingStopFailed(sessionHandle,
+                    UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(
+                            status),
+                    UwbSessionNotificationHelper.convertUciStatusToParam(
+                            uwbSession.getProtocolName(), status));
+            Log.i(TAG, "IUwbRangingCallbacks - onRangingStopFailed");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onRangingStopFailed : Failed");
+            e.printStackTrace();
+        }
+    }
+
+    public void onRangingReconfigured(UwbSession uwbSession) {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        PersistableBundle params;
+        if (Objects.equals(uwbSession.getProtocolName(), CccParams.PROTOCOL_NAME)) {
+            // Why are there no params defined for this bundle?
+            params = new CccRangingReconfiguredParams.Builder().build().toBundle();
+        } else {
+            // No params defined for FiRa reconfigure.
+            params = new PersistableBundle();
+        }
+        try {
+            uwbRangingCallbacks.onRangingReconfigured(sessionHandle, params);
+            Log.i(TAG, "IUwbRangingCallbacks - onRangingReconfigured");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onRangingReconfigured : Failed");
+            e.printStackTrace();
+        }
+    }
+
+    public void onRangingReconfigureFailed(UwbSession uwbSession, int status) {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        try {
+            uwbRangingCallbacks.onRangingReconfigureFailed(sessionHandle,
+                    UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(
+                            status),
+                    UwbSessionNotificationHelper.convertUciStatusToParam(
+                            uwbSession.getProtocolName(), status));
+            Log.i(TAG, "IUwbRangingCallbacks - onRangingReconfigureFailed");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onRangingReconfigureFailed : Failed");
+            e.printStackTrace();
+        }
+    }
+
+    public void onControleeAdded(UwbSession uwbSession) {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        try {
+            uwbRangingCallbacks.onControleeAdded(sessionHandle, new PersistableBundle());
+            Log.i(TAG, "IUwbRangingCallbacks - onControleeAdded");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onControleeAdded: Failed");
+            e.printStackTrace();
+        }
+    }
+
+    public void onControleeAddFailed(UwbSession uwbSession, int status) {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        try {
+            uwbRangingCallbacks.onControleeAddFailed(sessionHandle,
+                    UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(
+                            status),
+                    UwbSessionNotificationHelper.convertUciStatusToParam(
+                            uwbSession.getProtocolName(), status));
+            Log.i(TAG, "IUwbRangingCallbacks - onControleeAddFailed");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onControleeAddFailed : Failed");
+            e.printStackTrace();
+        }
+    }
+
+    public void onControleeRemoved(UwbSession uwbSession) {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        try {
+            uwbRangingCallbacks.onControleeRemoved(sessionHandle, new PersistableBundle());
+            Log.i(TAG, "IUwbRangingCallbacks - onControleeRemoved");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onControleeRemoved: Failed");
+            e.printStackTrace();
+        }
+    }
+
+    public void onControleeRemoveFailed(UwbSession uwbSession, int status) {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        try {
+            uwbRangingCallbacks.onControleeRemoveFailed(sessionHandle,
+                    UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(
+                            status),
+                    UwbSessionNotificationHelper.convertUciStatusToParam(
+                            uwbSession.getProtocolName(), status));
+            Log.i(TAG, "IUwbRangingCallbacks - onControleeRemoveFailed");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onControleeRemoveFailed : Failed");
+            e.printStackTrace();
+        }
+    }
+
+    public void onRangingClosed(UwbSession uwbSession, int status) {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        try {
+            uwbRangingCallbacks.onRangingClosed(sessionHandle,
+                    UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(
+                            status),
+                    UwbSessionNotificationHelper.convertUciStatusToParam(
+                            uwbSession.getProtocolName(), status));
+            Log.i(TAG, "IUwbRangingCallbacks - onRangingClosed");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onRangingClosed : Failed");
+            e.printStackTrace();
+        }
+    }
+
+    public void onRangingClosedWithApiReasonCode(
+            UwbSession uwbSession, @RangingChangeReason int reasonCode) {
+        SessionHandle sessionHandle = uwbSession.getSessionHandle();
+        IUwbRangingCallbacks uwbRangingCallbacks = uwbSession.getIUwbRangingCallbacks();
+        try {
+            uwbRangingCallbacks.onRangingClosed(sessionHandle, reasonCode, new PersistableBundle());
+            Log.i(TAG, "IUwbRangingCallbacks - onRangingClosed");
+        } catch (Exception e) {
+            Log.e(TAG, "IUwbRangingCallbacks - onRangingClosed : Failed");
+            e.printStackTrace();
+        }
+    }
+
+    private static RangingReport getRangingReport(
+            @NonNull UwbRangingData rangingData, String protocolName,
+            Params sessionParams, long elapsedRealtimeNanos) {
+        if (rangingData.getRangingMeasuresType()
+                != UwbUciConstants.RANGING_MEASUREMENT_TYPE_TWO_WAY) {
+            return null;
+        }
+        boolean isAoaAzimuthEnabled = true;
+        boolean isAoaElevationEnabled = true;
+        boolean isDestAoaAzimuthEnabled = false;
+        boolean isDestAoaElevationEnabled = false;
+        // For FIRA sessions, check if AOA is enabled for the session or not.
+        if (protocolName.equals(FiraParams.PROTOCOL_NAME)) {
+            FiraOpenSessionParams openSessionParams = (FiraOpenSessionParams) sessionParams;
+            switch (openSessionParams.getAoaResultRequest()) {
+                case FiraParams.AOA_RESULT_REQUEST_MODE_NO_AOA_REPORT:
+                    isAoaAzimuthEnabled = false;
+                    isAoaElevationEnabled = false;
+                    break;
+                case FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS:
+                case FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_INTERLEAVED:
+                    isAoaAzimuthEnabled = true;
+                    isAoaElevationEnabled = true;
+                    break;
+                case FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_AZIMUTH_ONLY:
+                    isAoaAzimuthEnabled = true;
+                    isAoaElevationEnabled = false;
+                    break;
+                case FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_ELEVATION_ONLY:
+                    isAoaAzimuthEnabled = false;
+                    isAoaElevationEnabled = true;
+                    break;
+                default:
+                    throw new IllegalArgumentException("Invalid AOA result req");
+            }
+            if (openSessionParams.hasResultReportPhase()) {
+                if (openSessionParams.hasAngleOfArrivalAzimuthReport()) {
+                    isDestAoaAzimuthEnabled = true;
+                }
+                if (openSessionParams.hasAngleOfArrivalElevationReport()) {
+                    isDestAoaElevationEnabled = true;
+                }
+            }
+        }
+        List<RangingMeasurement> rangingMeasurements = new ArrayList<>();
+        UwbTwoWayMeasurement[] uwbTwoWayMeasurement = rangingData.getRangingTwoWayMeasures();
+        for (int i = 0; i < rangingData.getNoOfRangingMeasures(); ++i) {
+            UwbAddress macAddress = UwbAddress.fromBytes(TlvUtil.getReverseBytes(
+                    uwbTwoWayMeasurement[i].getMacAddress()));
+            int rangingStatus = uwbTwoWayMeasurement[i].getRangingStatus();
+            DistanceMeasurement distanceMeasurement = null;
+            AngleOfArrivalMeasurement angleOfArrivalMeasurement = null;
+            AngleOfArrivalMeasurement destinationAngleOfArrivalMeasurement = null;
+            int los = uwbTwoWayMeasurement[i].mNLoS;
+
+            if (rangingStatus == FiraParams.STATUS_CODE_OK) {
+                // Distance measurement is mandatory
+                distanceMeasurement = new DistanceMeasurement.Builder()
+                        .setMeters(uwbTwoWayMeasurement[i].getDistance() / (double) 100)
+                        .setErrorMeters(0)
+                        // TODO: Need to fetch distance FOM once it is added to UCI spec.
+                        .setConfidenceLevel(0)
+                        .build();
+                // Aoa measurement is optional based on configuration.
+                if (isAoaAzimuthEnabled || isAoaElevationEnabled) {
+                    AngleMeasurement azimuthAngleMeasurement = null;
+                    AngleMeasurement altitudeAngleMeasurement = null;
+                    if (isAoaAzimuthEnabled) {
+                        azimuthAngleMeasurement = new AngleMeasurement(
+                                UwbUtil.degreeToRadian(uwbTwoWayMeasurement[i].getAoaAzimuth()),
+                                0, uwbTwoWayMeasurement[i].getAoaAzimuthFom() / (double) 100);
+                    }
+                    if (isAoaElevationEnabled) {
+                        altitudeAngleMeasurement = new AngleMeasurement(
+                                UwbUtil.degreeToRadian(uwbTwoWayMeasurement[i].getAoaElevation()),
+                                0, uwbTwoWayMeasurement[i].getAoaElevationFom() / (double) 100);
+                    }
+                    // AngleOfArrivalMeasurement
+                    angleOfArrivalMeasurement = new AngleOfArrivalMeasurement.Builder(
+                            azimuthAngleMeasurement)
+                            .setAltitude(altitudeAngleMeasurement)
+                            .build();
+                }
+                if (isDestAoaAzimuthEnabled || isDestAoaElevationEnabled) {
+                    AngleMeasurement destinationAzimuthAngleMeasurement = null;
+                    AngleMeasurement destinationAltitudeAngleMeasurement = null;
+                    if (isDestAoaAzimuthEnabled) {
+                        destinationAzimuthAngleMeasurement = new AngleMeasurement(
+                                UwbUtil.degreeToRadian(uwbTwoWayMeasurement[i].getAoaDestAzimuth()),
+                                0, uwbTwoWayMeasurement[i].getAoaDestAzimuthFom() / (double) 100);
+                    }
+                    if (isDestAoaElevationEnabled) {
+                        destinationAltitudeAngleMeasurement = new AngleMeasurement(
+                                UwbUtil.degreeToRadian(
+                                        uwbTwoWayMeasurement[i].getAoaDestElevation()),
+                                0, uwbTwoWayMeasurement[i].getAoaDestElevationFom() / (double) 100);
+                    }
+                    // Dest AngleOfArrivalMeasurement
+                    destinationAngleOfArrivalMeasurement = new AngleOfArrivalMeasurement.Builder(
+                            destinationAzimuthAngleMeasurement)
+                            .setAltitude(destinationAltitudeAngleMeasurement)
+                            .build();
+                }
+            }
+            rangingMeasurements.add(new RangingMeasurement.Builder()
+                    .setRemoteDeviceAddress(macAddress)
+                    .setStatus(rangingStatus)
+                    .setElapsedRealtimeNanos(elapsedRealtimeNanos)
+                    .setDistanceMeasurement(distanceMeasurement)
+                    .setAngleOfArrivalMeasurement(angleOfArrivalMeasurement)
+                    .setDestinationAngleOfArrivalMeasurement(destinationAngleOfArrivalMeasurement)
+                    .setLineOfSight(los)
+                    .build());
+        }
+        if (rangingMeasurements.size() == 1) {
+            return new RangingReport.Builder().addMeasurement(rangingMeasurements.get(0)).build();
+        } else {
+            return new RangingReport.Builder().addMeasurements(rangingMeasurements).build();
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbSettingsStore.java b/service/java/com/android/server/uwb/UwbSettingsStore.java
new file mode 100644
index 0000000..881c0a1
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbSettingsStore.java
@@ -0,0 +1,340 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import static android.uwb.UwbManager.AdapterStateCallback.STATE_ENABLED_ACTIVE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Handler;
+import android.os.PersistableBundle;
+import android.provider.Settings;
+import android.util.AtomicFile;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.uwb.util.FileUtils;
+
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Store data for storing UWB settings. These are key (string) / value pairs that are stored in
+ * UwbSettingsStore.xml file. The values allowed are those that can be serialized via
+ * {@link android.os.PersistableBundle}.
+ */
+public class UwbSettingsStore {
+    private static final String TAG = "UwbSettingsStore";
+    /**
+     * File name used for storing settings.
+     */
+    public static final String FILE_NAME = "UwbSettingsStore.xml";
+    /**
+     * Current config store data version. This will be incremented for any additions.
+     */
+    private static final int CURRENT_SETTINGS_STORE_DATA_VERSION = 1;
+    /** This list of older versions will be used to restore data from older store versions. */
+    /**
+     * First version of the config store data format.
+     */
+    private static final int INITIAL_SETTINGS_STORE_VERSION = 1;
+
+    /**
+     * Store the version of the data. This can be used to handle migration of data if some
+     * non-backward compatible change introduced.
+     */
+    private static final String VERSION_KEY = "version";
+
+    /**
+     * Constant copied over from {@link android.provider.Settings} since existing key is @hide.
+     */
+    @VisibleForTesting
+    public static final String SETTINGS_TOGGLE_STATE_KEY_FOR_MIGRATION = "uwb_enabled";
+
+    // List of all allowed keys.
+    private static final ArrayList<Key> sKeys = new ArrayList<>();
+
+    /******** Uwb shared pref keys ***************/
+    /**
+     * Store the UWB settings toggle state.
+     */
+    public static final Key<Boolean> SETTINGS_TOGGLE_STATE =
+            new Key<>("settings_toggle", true);
+
+    /******** Uwb shared pref keys ***************/
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final AtomicFile mAtomicFile;
+    private final UwbInjector mUwbInjector;
+
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private final PersistableBundle mSettings = new PersistableBundle();
+    @GuardedBy("mLock")
+    private final Map<String, Map<OnSettingsChangedListener, Handler>> mListeners =
+            new HashMap<>();
+
+    /**
+     * Interface for a settings change listener.
+     * @param <T> Type of the value.
+     */
+    public interface OnSettingsChangedListener<T> {
+        /**
+         * Invoked when a particular key settings changes.
+         *
+         * @param key Key that was changed.
+         * @param newValue New value that was assigned to the key.
+         */
+        void onSettingsChanged(@NonNull Key<T> key, @Nullable T newValue);
+    }
+
+    public UwbSettingsStore(@NonNull Context context, @NonNull Handler handler, @NonNull
+            AtomicFile atomicFile, UwbInjector uwbInjector) {
+        mContext = context;
+        mHandler = handler;
+        mAtomicFile = atomicFile;
+        mUwbInjector = uwbInjector;
+    }
+
+    /**
+     * Initialize the settings store by triggering the store file read.
+     */
+    public void initialize() {
+        Log.i(TAG, "Reading from store file: " + mAtomicFile.getBaseFile());
+        readFromStoreFile();
+        // Migrate toggle settings from Android 12 to Android 13.
+        boolean isStoreEmpty;
+        synchronized (mLock) {
+            isStoreEmpty = mSettings.isEmpty();
+        }
+        if (isStoreEmpty) {
+            try {
+                boolean toggleEnabled =
+                        mUwbInjector.getSettingsInt(SETTINGS_TOGGLE_STATE_KEY_FOR_MIGRATION)
+                                == STATE_ENABLED_ACTIVE;
+                Log.i(TAG, "Migrate settings toggle from older release: " + toggleEnabled);
+                put(SETTINGS_TOGGLE_STATE, toggleEnabled);
+            } catch (Settings.SettingNotFoundException e) {
+                /* ignore */
+            }
+        }
+        invokeAllListeners();
+    }
+
+    private void invokeAllListeners() {
+        synchronized (mLock) {
+            for (Key key : sKeys) {
+                invokeListeners(key);
+            }
+        }
+    }
+
+    private <T> void invokeListeners(@NonNull Key<T> key) {
+        synchronized (mLock) {
+            if (!mSettings.containsKey(key.key)) return;
+            Object newValue = mSettings.get(key.key);
+            Map<OnSettingsChangedListener, Handler> listeners = mListeners.get(key.key);
+            if (listeners == null || listeners.isEmpty()) return;
+            for (Map.Entry<OnSettingsChangedListener, Handler> listener
+                    : listeners.entrySet()) {
+                // Trigger the callback in the appropriate handler.
+                listener.getValue().post(() ->
+                        listener.getKey().onSettingsChanged(key, newValue));
+            }
+        }
+    }
+
+    /**
+     * Trigger config store writes and invoke listeners in the main service looper's handler.
+     */
+    private <T> void triggerSaveToStoreAndInvokeListeners(@NonNull Key<T> key) {
+        mHandler.post(() -> {
+            writeToStoreFile();
+            invokeListeners(key);
+        });
+    }
+
+    private void putObject(@NonNull String key, @Nullable Object value) {
+        synchronized (mLock) {
+            if (value == null) {
+                mSettings.putString(key, null);
+            } else if (value instanceof Boolean) {
+                mSettings.putBoolean(key, (Boolean) value);
+            } else if (value instanceof Integer) {
+                mSettings.putInt(key, (Integer) value);
+            } else if (value instanceof Long) {
+                mSettings.putLong(key, (Long) value);
+            } else if (value instanceof Double) {
+                mSettings.putDouble(key, (Double) value);
+            } else if (value instanceof String) {
+                mSettings.putString(key, (String) value);
+            } else {
+                throw new IllegalArgumentException("Unsupported type " + value.getClass());
+            }
+        }
+    }
+
+    private <T> T getObject(@NonNull String key, T defaultValue) {
+        Object value;
+        synchronized (mLock) {
+            if (defaultValue instanceof Boolean) {
+                value = mSettings.getBoolean(key, (Boolean) defaultValue);
+            } else if (defaultValue instanceof Integer) {
+                value = mSettings.getInt(key, (Integer) defaultValue);
+            } else if (defaultValue instanceof Long) {
+                value = mSettings.getLong(key, (Long) defaultValue);
+            } else if (defaultValue instanceof Double) {
+                value = mSettings.getDouble(key, (Double) defaultValue);
+            } else if (defaultValue instanceof String) {
+                value = mSettings.getString(key, (String) defaultValue);
+            } else {
+                throw new IllegalArgumentException("Unsupported type " + defaultValue.getClass());
+            }
+        }
+        return (T) value;
+    }
+
+    /**
+     * Store a value to the stored settings.
+     *
+     * @param key One of the settings keys.
+     * @param value Value to be stored.
+     */
+    public <T> void put(@NonNull Key<T> key, @Nullable T value) {
+        putObject(key.key, value);
+        triggerSaveToStoreAndInvokeListeners(key);
+    }
+
+    /**
+     * Retrieve a value from the stored settings.
+     *
+     * @param key One of the settings keys.
+     * @return value stored in settings, defValue if the key does not exist.
+     */
+    public @Nullable <T> T get(@NonNull Key<T> key) {
+        return getObject(key.key, key.defaultValue);
+    }
+
+    /**
+     * Register for settings change listener.
+     *
+     * @param key One of the settings keys.
+     * @param listener Listener to be registered.
+     * @param handler Handler to post the listener
+     */
+    public <T> void registerChangeListener(@NonNull Key<T> key,
+            @NonNull OnSettingsChangedListener<T> listener, @NonNull Handler handler) {
+        synchronized (mLock) {
+            mListeners.computeIfAbsent(
+                    key.key, ignore -> new HashMap<>()).put(listener, handler);
+        }
+    }
+
+    /**
+     * Unregister for settings change listener.
+     *
+     * @param key One of the settings keys.
+     * @param listener Listener to be unregistered.
+     */
+    public <T> void unregisterChangeListener(@NonNull Key<T> key,
+            @NonNull OnSettingsChangedListener<T> listener) {
+        synchronized (mLock) {
+            Map<OnSettingsChangedListener, Handler> listeners = mListeners.get(key.key);
+            if (listeners == null || listeners.isEmpty()) {
+                Log.e(TAG, "No listeners for " + key);
+                return;
+            }
+            if (listeners.remove(listener) == null) {
+                Log.e(TAG, "Unknown listener for " + key);
+            }
+        }
+    }
+
+    /**
+     * Dump output for debugging.
+     */
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println();
+        pw.println("Dump of " + TAG);
+        synchronized (mLock) {
+            pw.println("Settings: " + mSettings);
+        }
+    }
+
+    /**
+     * Base class to store string key and its default value.
+     * @param <T> Type of the value.
+     */
+    public static class Key<T> {
+        public final String key;
+        public final T defaultValue;
+
+        private Key(@NonNull String key, T defaultValue) {
+            this.key = key;
+            this.defaultValue = defaultValue;
+            sKeys.add(this);
+        }
+
+        @Override
+        public String toString() {
+            return "[Key " + key + ", DefaultValue: " + defaultValue + "]";
+        }
+    }
+
+    private void writeToStoreFile() {
+        try {
+            final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+            final PersistableBundle bundleToWrite;
+            synchronized (mLock) {
+                bundleToWrite = new PersistableBundle(mSettings);
+            }
+            bundleToWrite.putInt(VERSION_KEY, CURRENT_SETTINGS_STORE_DATA_VERSION);
+            bundleToWrite.writeToStream(outputStream);
+            FileUtils.writeToAtomicFile(mAtomicFile, outputStream.toByteArray());
+        } catch (IOException e) {
+            Log.e(TAG, "Write to store file failed", e);
+        }
+    }
+
+    private void readFromStoreFile() {
+        try {
+            final byte[] readData = FileUtils.readFromAtomicFile(mAtomicFile);
+            final ByteArrayInputStream inputStream = new ByteArrayInputStream(readData);
+            final PersistableBundle bundleRead = PersistableBundle.readFromStream(inputStream);
+            // Version unused for now. May be needed in the future for handling migrations.
+            bundleRead.remove(VERSION_KEY);
+            synchronized (mLock) {
+                mSettings.putAll(bundleRead);
+            }
+        } catch (FileNotFoundException e) {
+            Log.w(TAG, "No store file to read");
+        } catch (IOException e) {
+            Log.e(TAG, "Read from store file failed", e);
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbShellCommand.java b/service/java/com/android/server/uwb/UwbShellCommand.java
new file mode 100644
index 0000000..b7cead9
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbShellCommand.java
@@ -0,0 +1,945 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import static android.uwb.UwbAddress.SHORT_ADDRESS_BYTE_LENGTH;
+
+import static com.google.uwb.support.ccc.CccParams.CHAPS_PER_SLOT_3;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_CONFIG_MODE_ADAPTIVE;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_CONFIG_MODE_CONTINUOUS;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_SEQUENCE_AES;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_SEQUENCE_DEFAULT;
+import static com.google.uwb.support.ccc.CccParams.PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE;
+import static com.google.uwb.support.ccc.CccParams.SLOTS_PER_ROUND_6;
+import static com.google.uwb.support.ccc.CccParams.UWB_CHANNEL_9;
+import static com.google.uwb.support.fira.FiraParams.AOA_RESULT_REQUEST_MODE_NO_AOA_REPORT;
+import static com.google.uwb.support.fira.FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS;
+import static com.google.uwb.support.fira.FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_AZIMUTH_ONLY;
+import static com.google.uwb.support.fira.FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_ELEVATION_ONLY;
+import static com.google.uwb.support.fira.FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_INTERLEAVED;
+import static com.google.uwb.support.fira.FiraParams.HOPPING_MODE_DISABLE;
+import static com.google.uwb.support.fira.FiraParams.MULTICAST_LIST_UPDATE_ACTION_ADD;
+import static com.google.uwb.support.fira.FiraParams.MULTICAST_LIST_UPDATE_ACTION_DELETE;
+import static com.google.uwb.support.fira.FiraParams.MULTI_NODE_MODE_ONE_TO_MANY;
+import static com.google.uwb.support.fira.FiraParams.MULTI_NODE_MODE_UNICAST;
+import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_ROLE_INITIATOR;
+import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_ROLE_RESPONDER;
+import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_TYPE_CONTROLEE;
+import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_TYPE_CONTROLLER;
+import static com.google.uwb.support.fira.FiraParams.RANGING_ROUND_USAGE_DS_TWR_DEFERRED_MODE;
+import static com.google.uwb.support.fira.FiraParams.RANGING_ROUND_USAGE_DS_TWR_NON_DEFERRED_MODE;
+import static com.google.uwb.support.fira.FiraParams.RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE;
+import static com.google.uwb.support.fira.FiraParams.RANGING_ROUND_USAGE_SS_TWR_NON_DEFERRED_MODE;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.annotation.NonNull;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Binder;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Pair;
+import android.uwb.IUwbRangingCallbacks;
+import android.uwb.RangingReport;
+import android.uwb.SessionHandle;
+import android.uwb.UwbAddress;
+import android.uwb.UwbManager;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.BasicShellCommandHandler;
+import com.android.server.uwb.jni.NativeUwbManager;
+import com.android.server.uwb.util.ArrayUtils;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.ccc.CccOpenRangingParams;
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.ccc.CccPulseShapeCombo;
+import com.google.uwb.support.ccc.CccStartRangingParams;
+import com.google.uwb.support.fira.FiraOpenSessionParams;
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraRangingReconfigureParams;
+import com.google.uwb.support.generic.GenericSpecificationParams;
+
+import java.io.PrintWriter;
+import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Interprets and executes 'adb shell cmd uwb [args]'.
+ *
+ * To add new commands:
+ * - onCommand: Add a case "<command>" execute. Return a 0
+ *   if command executed successfully.
+ * - onHelp: add a description string.
+ *
+ * Permissions: currently root permission is required for some commands. Others will
+ * enforce the corresponding API permissions.
+ */
+public class UwbShellCommand extends BasicShellCommandHandler {
+    @VisibleForTesting
+    public static String SHELL_PACKAGE_NAME = "com.android.shell";
+    private static final long RANGE_CTL_TIMEOUT_MILLIS = 10_000;
+
+    // These don't require root access.
+    // However, these do perform permission checks in the corresponding UwbService methods.
+    private static final String[] NON_PRIVILEGED_COMMANDS = {
+            "help",
+            "status",
+            "get-country-code",
+            "enable-uwb",
+            "disable-uwb",
+            "start-fira-ranging-session",
+            "start-ccc-ranging-session",
+            "reconfigure-fira-ranging-session",
+            "get-ranging-session-reports",
+            "get-all-ranging-session-reports",
+            "stop-ranging-session",
+            "stop-all-ranging-sessions",
+            "get-specification-info",
+    };
+
+    @VisibleForTesting
+    public static final FiraOpenSessionParams.Builder DEFAULT_FIRA_OPEN_SESSION_PARAMS =
+            new FiraOpenSessionParams.Builder()
+                    .setProtocolVersion(FiraParams.PROTOCOL_VERSION_1_1)
+                    .setSessionId(1)
+                    .setChannelNumber(9)
+                    .setDeviceType(RANGING_DEVICE_TYPE_CONTROLLER)
+                    .setDeviceRole(RANGING_DEVICE_ROLE_INITIATOR)
+                    .setDeviceAddress(UwbAddress.fromBytes(new byte[] { 0x4, 0x6}))
+                    .setDestAddressList(Arrays.asList(UwbAddress.fromBytes(new byte[] { 0x4, 0x6})))
+                    .setMultiNodeMode(MULTI_NODE_MODE_UNICAST)
+                    .setRangingRoundUsage(RANGING_ROUND_USAGE_DS_TWR_DEFERRED_MODE)
+                    .setVendorId(new byte[]{0x5, 0x78})
+                    .setStaticStsIV(new byte[]{0x1a, 0x55, 0x77, 0x47, 0x7e, 0x7d});
+
+    @VisibleForTesting
+    public static final CccOpenRangingParams.Builder DEFAULT_CCC_OPEN_RANGING_PARAMS =
+            new CccOpenRangingParams.Builder()
+                    .setProtocolVersion(CccParams.PROTOCOL_VERSION_1_0)
+                    .setUwbConfig(CccParams.UWB_CONFIG_0)
+                    .setPulseShapeCombo(
+                            new CccPulseShapeCombo(
+                                    PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE,
+                                    PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE))
+                    .setSessionId(1)
+                    .setRanMultiplier(4)
+                    .setChannel(UWB_CHANNEL_9)
+                    .setNumChapsPerSlot(CHAPS_PER_SLOT_3)
+                    .setNumResponderNodes(1)
+                    .setNumSlotsPerRound(SLOTS_PER_ROUND_6)
+                    .setSyncCodeIndex(1)
+                    .setHoppingConfigMode(HOPPING_MODE_DISABLE)
+                    .setHoppingSequence(HOPPING_SEQUENCE_DEFAULT);
+
+    private static final Map<Integer, SessionInfo> sSessionIdToInfo = new ArrayMap<>();
+    private static int sSessionHandleIdNext = 0;
+
+    private final UwbServiceImpl mUwbService;
+    private final UwbCountryCode mUwbCountryCode;
+    private final NativeUwbManager mNativeUwbManager;
+    private final Context mContext;
+
+    UwbShellCommand(UwbInjector uwbInjector, UwbServiceImpl uwbService, Context context) {
+        mUwbService = uwbService;
+        mContext = context;
+        mUwbCountryCode = uwbInjector.getUwbCountryCode();
+        mNativeUwbManager = uwbInjector.getNativeUwbManager();
+    }
+
+    private static String bundleToString(@Nullable PersistableBundle bundle) {
+        if (bundle != null) {
+            // Need to defuse any local bundles before printing. Use isEmpty() triggers unparcel.
+            bundle.isEmpty();
+            return bundle.toString();
+        } else {
+            return "null";
+        }
+    }
+
+    private static final class UwbRangingCallbacks extends IUwbRangingCallbacks.Stub {
+        private final SessionInfo mSessionInfo;
+        private final PrintWriter mPw;
+        private final CompletableFuture mRangingOpenedFuture;
+        private final CompletableFuture mRangingStartedFuture;
+        private final CompletableFuture mRangingStoppedFuture;
+        private final CompletableFuture mRangingClosedFuture;
+        private final CompletableFuture mRangingReconfiguredFuture;
+
+        UwbRangingCallbacks(@NonNull SessionInfo sessionInfo, @NonNull PrintWriter pw,
+                @NonNull CompletableFuture rangingOpenedFuture,
+                @NonNull CompletableFuture rangingStartedFuture,
+                @NonNull CompletableFuture rangingStoppedFuture,
+                @NonNull CompletableFuture rangingClosedFuture,
+                @NonNull CompletableFuture rangingReconfiguredFuture) {
+            mSessionInfo = sessionInfo;
+            mPw = pw;
+            mRangingOpenedFuture = rangingOpenedFuture;
+            mRangingStartedFuture = rangingStartedFuture;
+            mRangingStoppedFuture = rangingStoppedFuture;
+            mRangingClosedFuture = rangingClosedFuture;
+            mRangingReconfiguredFuture = rangingReconfiguredFuture;
+        }
+
+        public void onRangingOpened(SessionHandle sessionHandle) {
+            mPw.println("Ranging session opened");
+            mRangingOpenedFuture.complete(true);
+        }
+
+        public void onRangingOpenFailed(SessionHandle sessionHandle, int reason,
+                PersistableBundle params) {
+            mPw.println("Ranging session open failed with reason: " + reason + " and params: "
+                    + bundleToString(params));
+            mRangingOpenedFuture.complete(false);
+        }
+
+        public void onRangingStarted(SessionHandle sessionHandle, PersistableBundle params) {
+            mPw.println("Ranging session started with params: " + bundleToString(params));
+            mRangingStartedFuture.complete(true);
+        }
+
+        public void onRangingStartFailed(SessionHandle sessionHandle, int reason,
+                PersistableBundle params) {
+            mPw.println("Ranging session start failed with reason: " + reason + " and params: "
+                    + bundleToString(params));
+            mRangingStartedFuture.complete(false);
+        }
+
+        public void onRangingReconfigured(SessionHandle sessionHandle, PersistableBundle params) {
+            mPw.println("Ranging reconfigured with params: " + bundleToString(params));
+            mRangingReconfiguredFuture.complete(true);
+        }
+
+        public void onRangingReconfigureFailed(SessionHandle sessionHandle, int reason,
+                PersistableBundle params) {
+            mPw.println("Ranging reconfigure failed with reason: " + reason + " and params: "
+                    + bundleToString(params));
+            mRangingReconfiguredFuture.complete(true);
+
+        }
+
+        public void onRangingStopped(SessionHandle sessionHandle, int reason,
+                PersistableBundle params) {
+            mPw.println("Ranging session stopped with reason: " + reason + " and params: "
+                    + bundleToString(params));
+            mRangingStoppedFuture.complete(true);
+        }
+
+        public void onRangingStopFailed(SessionHandle sessionHandle, int reason,
+                PersistableBundle params) {
+            mPw.println("Ranging session stop failed with reason: " + reason + " and params: "
+                    + bundleToString(params));
+            mRangingStoppedFuture.complete(false);
+        }
+
+        public void onRangingClosed(SessionHandle sessionHandle, int reason,
+                PersistableBundle params) {
+            mPw.println("Ranging session closed with reason: " + reason + " and params: "
+                    + bundleToString(params));
+            sSessionIdToInfo.remove(mSessionInfo.sessionId);
+            mRangingClosedFuture.complete(true);
+        }
+
+        public void onRangingResult(SessionHandle sessionHandle, RangingReport rangingReport) {
+            mPw.println("Ranging Result: " + rangingReport);
+            mSessionInfo.addRangingReport(rangingReport);
+        }
+
+        public void onControleeAdded(SessionHandle sessionHandle, PersistableBundle params) {}
+
+        public void onControleeAddFailed(SessionHandle sessionHandle, int reason,
+                PersistableBundle params) {}
+
+        public void onControleeRemoved(SessionHandle sessionHandle, PersistableBundle params) {}
+
+        public void onControleeRemoveFailed(SessionHandle sessionHandle, int reason,
+                PersistableBundle params) {}
+
+        public void onRangingPaused(SessionHandle sessionHandle, PersistableBundle params) {}
+
+        public void onRangingPauseFailed(SessionHandle sessionHandle, int reason,
+                PersistableBundle params) {}
+
+        public void onRangingResumed(SessionHandle sessionHandle, PersistableBundle params) {}
+
+        public void onRangingResumeFailed(SessionHandle sessionHandle, int reason,
+                PersistableBundle params) {}
+
+        public void onDataSent(SessionHandle sessionHandle, UwbAddress uwbAddress,
+                PersistableBundle params) {}
+
+        public void onDataSendFailed(SessionHandle sessionHandle, UwbAddress uwbAddress, int reason,
+                PersistableBundle params) {}
+
+        public void onDataReceived(SessionHandle sessionHandle, UwbAddress uwbAddress,
+                PersistableBundle params, byte[] data) {}
+
+        public void onDataReceiveFailed(SessionHandle sessionHandle, UwbAddress uwbAddress,
+                int reason, PersistableBundle params) {}
+
+        public void onServiceDiscovered(SessionHandle sessionHandle, PersistableBundle params) {}
+
+        public void onServiceConnected(SessionHandle sessionHandle, PersistableBundle params) {}
+    }
+
+
+    private class SessionInfo {
+        private static final int LAST_NUM_RANGING_REPORTS = 20;
+
+        public final SessionHandle sessionHandle;
+        public final int sessionId;
+        public final Params openRangingParams;
+        public final UwbRangingCallbacks uwbRangingCbs;
+        public final ArrayDeque<RangingReport> lastRangingReports =
+                new ArrayDeque<>(LAST_NUM_RANGING_REPORTS);
+
+        public final CompletableFuture<Boolean> rangingOpenedFuture = new CompletableFuture<>();
+        public final CompletableFuture<Boolean> rangingStartedFuture = new CompletableFuture<>();
+        public final CompletableFuture<Boolean> rangingStoppedFuture = new CompletableFuture<>();
+        public final CompletableFuture<Boolean> rangingClosedFuture = new CompletableFuture<>();
+        public final CompletableFuture<Boolean> rangingReconfiguredFuture =
+                new CompletableFuture<>();
+
+        SessionInfo(int sessionId, int sSessionHandleIdNext, @NonNull Params openRangingParams,
+                @NonNull PrintWriter pw) {
+            this.sessionId = sessionId;
+            sessionHandle = new SessionHandle(sSessionHandleIdNext);
+            this.openRangingParams = openRangingParams;
+            uwbRangingCbs = new UwbRangingCallbacks(this, pw, rangingOpenedFuture,
+                    rangingStartedFuture, rangingStoppedFuture, rangingClosedFuture,
+                    rangingReconfiguredFuture);
+        }
+
+        public void addRangingReport(@NonNull RangingReport rangingReport) {
+            if (lastRangingReports.size() == LAST_NUM_RANGING_REPORTS) {
+                lastRangingReports.remove();
+            }
+            lastRangingReports.add(rangingReport);
+        }
+    }
+
+    private Pair<FiraOpenSessionParams, Boolean> buildFiraOpenSessionParams() {
+        FiraOpenSessionParams.Builder builder =
+                new FiraOpenSessionParams.Builder(DEFAULT_FIRA_OPEN_SESSION_PARAMS);
+        boolean shouldBlockCall = false;
+        boolean interleavingEnabled = false;
+        boolean aoaResultReqEnabled = false;
+        String option = getNextOption();
+        while (option != null) {
+            if (option.equals("-b")) {
+                shouldBlockCall = true;
+            }
+            if (option.equals("-i")) {
+                builder.setSessionId(Integer.parseInt(getNextArgRequired()));
+            }
+            if (option.equals("-c")) {
+                builder.setChannelNumber(Integer.parseInt(getNextArgRequired()));
+            }
+            if (option.equals("-t")) {
+                String type = getNextArgRequired();
+                if (type.equals("controller")) {
+                    builder.setDeviceType(RANGING_DEVICE_TYPE_CONTROLLER);
+                } else if (type.equals("controlee")) {
+                    builder.setDeviceType(RANGING_DEVICE_TYPE_CONTROLEE);
+                } else {
+                    throw new IllegalArgumentException("Unknown device type: " + type);
+                }
+            }
+            if (option.equals("-r")) {
+                String role = getNextArgRequired();
+                if (role.equals("initiator")) {
+                    builder.setDeviceType(RANGING_DEVICE_ROLE_INITIATOR);
+                } else if (role.equals("responder")) {
+                    builder.setDeviceType(RANGING_DEVICE_ROLE_RESPONDER);
+                } else {
+                    throw new IllegalArgumentException("Unknown device role: " + role);
+                }
+            }
+            if (option.equals("-a")) {
+                builder.setDeviceAddress(
+                        UwbAddress.fromBytes(
+                                ByteBuffer.allocate(SHORT_ADDRESS_BYTE_LENGTH)
+                                        .putShort(Short.parseShort(getNextArgRequired()))
+                                        .array()));
+            }
+            if (option.equals("-d")) {
+                String[] destAddressesString = getNextArgRequired().split(",");
+                List<UwbAddress> destAddresses = new ArrayList<>();
+                for (String destAddressString : destAddressesString) {
+                    destAddresses.add(UwbAddress.fromBytes(
+                            ByteBuffer.allocate(SHORT_ADDRESS_BYTE_LENGTH)
+                                    .putShort(Short.parseShort(destAddressString))
+                                    .array()));
+                }
+                builder.setDestAddressList(destAddresses);
+                builder.setMultiNodeMode(destAddresses.size() > 1
+                        ? MULTI_NODE_MODE_ONE_TO_MANY
+                        : MULTI_NODE_MODE_UNICAST);
+            }
+            if (option.equals("-u")) {
+                String usage = getNextArgRequired();
+                if (usage.equals("ds-twr")) {
+                    builder.setRangingRoundUsage(RANGING_ROUND_USAGE_DS_TWR_DEFERRED_MODE);
+                } else if (usage.equals("ss-twr")) {
+                    builder.setRangingRoundUsage(RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE);
+                } else if (usage.equals("ds-twr-non-deferred")) {
+                    builder.setRangingRoundUsage(RANGING_ROUND_USAGE_DS_TWR_NON_DEFERRED_MODE);
+                } else if (usage.equals("ss-twr-non-deferred")) {
+                    builder.setRangingRoundUsage(RANGING_ROUND_USAGE_SS_TWR_NON_DEFERRED_MODE);
+                } else {
+                    throw new IllegalArgumentException("Unknown round usage: " + usage);
+                }
+            }
+            if (option.equals("-z")) {
+                String[] interleaveRatioString = getNextArgRequired().split(",");
+                if (interleaveRatioString.length != 3) {
+                    throw new IllegalArgumentException("Unexpected interleaving ratio: "
+                            +  Arrays.toString(interleaveRatioString)
+                            + " expected to be <numRange, numAoaAzimuth, numAoaElevation>");
+                }
+                int numOfRangeMsrmts = Integer.parseInt(interleaveRatioString[0]);
+                int numOfAoaAzimuthMrmts = Integer.parseInt(interleaveRatioString[1]);
+                int numOfAoaElevationMrmts = Integer.parseInt(interleaveRatioString[2]);
+                // Set to interleaving mode
+                builder.setAoaResultRequest(AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_INTERLEAVED);
+                builder.setMeasurementFocusRatio(
+                        numOfRangeMsrmts,
+                        numOfAoaAzimuthMrmts,
+                        numOfAoaElevationMrmts);
+                interleavingEnabled = true;
+            }
+            if (option.equals("-e")) {
+                String aoaType = getNextArgRequired();
+                if (aoaType.equals("none")) {
+                    builder.setAoaResultRequest(AOA_RESULT_REQUEST_MODE_NO_AOA_REPORT);
+                } else if (aoaType.equals("enabled")) {
+                    builder.setAoaResultRequest(AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS);
+                } else if (aoaType.equals("azimuth-only")) {
+                    builder.setAoaResultRequest(
+                        AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_AZIMUTH_ONLY);
+                } else if (aoaType.equals("elevation-only")) {
+                    builder.setAoaResultRequest(
+                        AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_ELEVATION_ONLY);
+                } else {
+                    throw new IllegalArgumentException("Unknown aoa type: " + aoaType);
+                }
+                aoaResultReqEnabled = true;
+            }
+            option = getNextOption();
+        }
+        if (aoaResultReqEnabled && interleavingEnabled) {
+            throw new IllegalArgumentException(
+                    "Both interleaving (-z) and aoa result req (-e) cannot be specified");
+        }
+        // TODO: Add remaining params if needed.
+        return Pair.create(builder.build(), shouldBlockCall);
+    }
+
+    private void startFiraRangingSession(PrintWriter pw) throws Exception {
+        Pair<FiraOpenSessionParams, Boolean> firaOpenSessionParams = buildFiraOpenSessionParams();
+        startRangingSession(
+                firaOpenSessionParams.first, null, firaOpenSessionParams.first.getSessionId(),
+                firaOpenSessionParams.second, pw);
+    }
+
+    private Pair<CccOpenRangingParams, Boolean> buildCccOpenRangingParams() {
+        CccOpenRangingParams.Builder builder =
+                new CccOpenRangingParams.Builder(DEFAULT_CCC_OPEN_RANGING_PARAMS);
+        boolean shouldBlockCall = false;
+        String option = getNextOption();
+        while (option != null) {
+            if (option.equals("-b")) {
+                shouldBlockCall = true;
+            }
+            if (option.equals("-u")) {
+                builder.setUwbConfig(Integer.parseInt(getNextArgRequired()));
+            }
+            if (option.equals("-p")) {
+                String[] pulseComboString = getNextArgRequired().split(",");
+                if (pulseComboString.length != 2) {
+                    throw new IllegalArgumentException("Erroneous pulse combo: "
+                            + Arrays.toString(pulseComboString));
+                }
+                builder.setPulseShapeCombo(new CccPulseShapeCombo(
+                        Integer.parseInt(pulseComboString[0]),
+                        Integer.parseInt(pulseComboString[1])));
+            }
+            if (option.equals("-i")) {
+                builder.setSessionId(Integer.parseInt(getNextArgRequired()));
+            }
+            if (option.equals("-r")) {
+                builder.setRanMultiplier(Integer.parseInt(getNextArgRequired()));
+            }
+            if (option.equals("-c")) {
+                builder.setChannel(Integer.parseInt(getNextArgRequired()));
+            }
+            if (option.equals("-p")) {
+                builder.setNumChapsPerSlot(Integer.parseInt(getNextArgRequired()));
+            }
+            if (option.equals("-n")) {
+                builder.setNumResponderNodes(Integer.parseInt(getNextArgRequired()));
+            }
+            if (option.equals("-o")) {
+                builder.setNumSlotsPerRound(Integer.parseInt(getNextArgRequired()));
+            }
+            if (option.equals("-s")) {
+                builder.setSyncCodeIndex(Integer.parseInt(getNextArgRequired()));
+            }
+            if (option.equals("-h")) {
+                String hoppingConfigMode = getNextArgRequired();
+                if (hoppingConfigMode.equals("none")) {
+                    builder.setHoppingConfigMode(HOPPING_MODE_DISABLE);
+                } else if (hoppingConfigMode.equals("continuous")) {
+                    builder.setHoppingConfigMode(HOPPING_CONFIG_MODE_CONTINUOUS);
+                } else if (hoppingConfigMode.equals("adaptive")) {
+                    builder.setHoppingConfigMode(HOPPING_CONFIG_MODE_ADAPTIVE);
+                } else {
+                    throw new IllegalArgumentException("Unknown hopping config mode: "
+                            + hoppingConfigMode);
+                }
+            }
+            if (option.equals("-a")) {
+                String hoppingSequence = getNextArgRequired();
+                if (hoppingSequence.equals("default")) {
+                    builder.setHoppingSequence(HOPPING_SEQUENCE_DEFAULT);
+                } else if (hoppingSequence.equals("aes")) {
+                    builder.setHoppingConfigMode(HOPPING_SEQUENCE_AES);
+                } else {
+                    throw new IllegalArgumentException("Unknown hopping sequence: "
+                            + hoppingSequence);
+                }
+            }
+            option = getNextOption();
+        }
+        // TODO: Add remaining params if needed.
+        return Pair.create(builder.build(), shouldBlockCall);
+    }
+
+    private void startCccRangingSession(PrintWriter pw) throws Exception {
+        Pair<CccOpenRangingParams, Boolean> cccOpenRangingParamsAndBlocking =
+                buildCccOpenRangingParams();
+        CccOpenRangingParams cccOpenRangingParams = cccOpenRangingParamsAndBlocking.first;
+        CccStartRangingParams cccStartRangingParams = new CccStartRangingParams.Builder()
+                .setSessionId(cccOpenRangingParams.getSessionId())
+                .setRanMultiplier(cccOpenRangingParams.getRanMultiplier())
+                .build();
+        startRangingSession(
+                cccOpenRangingParams, cccStartRangingParams, cccOpenRangingParams.getSessionId(),
+                cccOpenRangingParamsAndBlocking.second, pw);
+    }
+
+    private void startRangingSession(@NonNull Params openRangingSessionParams,
+            @Nullable Params startRangingSessionParams, int sessionId,
+            boolean shouldBlockCall, @NonNull PrintWriter pw) throws Exception {
+        if (sSessionIdToInfo.containsKey(sessionId)) {
+            pw.println("Session with session ID: " + sessionId
+                    + " already ongoing. Stop that session before you start a new session");
+            return;
+        }
+        SessionInfo sessionInfo =
+                new SessionInfo(sessionId, sSessionHandleIdNext++, openRangingSessionParams, pw);
+        mUwbService.openRanging(
+                new AttributionSource.Builder(Process.SHELL_UID)
+                        .setPackageName(SHELL_PACKAGE_NAME)
+                        .build(),
+                sessionInfo.sessionHandle,
+                sessionInfo.uwbRangingCbs,
+                openRangingSessionParams.toBundle(),
+                null);
+        boolean openCompleted = false;
+        try {
+            openCompleted = sessionInfo.rangingOpenedFuture.get(
+                    RANGE_CTL_TIMEOUT_MILLIS, MILLISECONDS);
+        } catch (InterruptedException | CancellationException | TimeoutException
+                | ExecutionException e) {
+        }
+        if (!openCompleted) {
+            pw.println("Failed to open ranging session. Aborting!");
+            return;
+        }
+        pw.println("Ranging session opened with params: "
+                + bundleToString(openRangingSessionParams.toBundle()));
+
+        mUwbService.startRanging(
+                sessionInfo.sessionHandle,
+                startRangingSessionParams != null
+                        ? startRangingSessionParams.toBundle()
+                        : new PersistableBundle());
+        boolean startCompleted = false;
+        try {
+            startCompleted = sessionInfo.rangingStartedFuture.get(
+                    RANGE_CTL_TIMEOUT_MILLIS, MILLISECONDS);
+        } catch (InterruptedException | CancellationException | TimeoutException
+                | ExecutionException e) {
+        }
+        if (!startCompleted) {
+            pw.println("Failed to start ranging session. Aborting!");
+            return;
+        }
+        pw.println("Ranging session started for sessionId: " + sessionId);
+        sSessionIdToInfo.put(sessionId, sessionInfo);
+        while (shouldBlockCall) {
+            Thread.sleep(RANGE_CTL_TIMEOUT_MILLIS);
+        }
+    }
+
+    private void stopRangingSession(PrintWriter pw) throws RemoteException {
+        int sessionId = Integer.parseInt(getNextArgRequired());
+        stopRangingSession(pw, sessionId);
+    }
+
+    private void stopRangingSession(PrintWriter pw, int sessionId) throws RemoteException {
+        SessionInfo sessionInfo = sSessionIdToInfo.get(sessionId);
+        if (sessionInfo == null) {
+            pw.println("No active session with session ID: " + sessionId + " found");
+            return;
+        }
+        mUwbService.stopRanging(sessionInfo.sessionHandle);
+        boolean stopCompleted = false;
+        try {
+            stopCompleted = sessionInfo.rangingStoppedFuture.get(
+                    RANGE_CTL_TIMEOUT_MILLIS, MILLISECONDS);
+        } catch (InterruptedException | CancellationException | TimeoutException
+                | ExecutionException e) {
+        }
+        if (!stopCompleted) {
+            pw.println("Failed to stop ranging session. Aborting!");
+            return;
+        }
+        pw.println("Ranging session stopped");
+
+        mUwbService.closeRanging(sessionInfo.sessionHandle);
+        boolean closeCompleted = false;
+        try {
+            closeCompleted = sessionInfo.rangingClosedFuture.get(
+                    RANGE_CTL_TIMEOUT_MILLIS, MILLISECONDS);
+        } catch (InterruptedException | CancellationException | TimeoutException
+                | ExecutionException e) {
+        }
+        if (!closeCompleted) {
+            pw.println("Failed to close ranging session. Aborting!");
+            return;
+        }
+        pw.println("Ranging session closed");
+    }
+
+    private FiraRangingReconfigureParams buildFiraReconfigureParams() {
+        FiraRangingReconfigureParams.Builder builder =
+                new FiraRangingReconfigureParams.Builder();
+        // defaults
+        builder.setAction(MULTICAST_LIST_UPDATE_ACTION_ADD);
+
+        String option = getNextOption();
+        while (option != null) {
+            if (option.equals("-a")) {
+                String action = getNextArgRequired();
+                if (action.equals("add")) {
+                    builder.setAction(MULTICAST_LIST_UPDATE_ACTION_ADD);
+                } else if (action.equals("delete")) {
+                    builder.setAction(MULTICAST_LIST_UPDATE_ACTION_DELETE);
+                } else {
+                    throw new IllegalArgumentException("Unexpected action " + action);
+                }
+            }
+            if (option.equals("-d")) {
+                String[] destAddressesString = getNextArgRequired().split(",");
+                List<UwbAddress> destAddresses = new ArrayList<>();
+                for (String destAddressString : destAddressesString) {
+                    destAddresses.add(UwbAddress.fromBytes(
+                            ByteBuffer.allocate(SHORT_ADDRESS_BYTE_LENGTH)
+                                    .putShort(Short.parseShort(destAddressString))
+                                    .array()));
+                }
+                builder.setAddressList(destAddresses.toArray(new UwbAddress[0]));
+            }
+            if (option.equals("-s")) {
+                String[] subSessionIdsString = getNextArgRequired().split(",");
+                List<Integer> subSessionIds = new ArrayList<>();
+                for (String subSessionIdString : subSessionIdsString) {
+                    subSessionIds.add(Integer.parseInt(subSessionIdString));
+                }
+                builder.setSubSessionIdList(subSessionIds.stream().mapToInt(s -> s).toArray());
+            }
+            option = getNextOption();
+        }
+        // TODO: Add remaining params if needed.
+        return builder.build();
+    }
+
+    private void reconfigureFiraRangingSession(PrintWriter pw) throws RemoteException {
+        int sessionId = Integer.parseInt(getNextArgRequired());
+        SessionInfo sessionInfo = sSessionIdToInfo.get(sessionId);
+        if (sessionInfo == null) {
+            pw.println("No active session with session ID: " + sessionId + " found");
+            return;
+        }
+        FiraRangingReconfigureParams params = buildFiraReconfigureParams();
+
+        mUwbService.reconfigureRanging(sessionInfo.sessionHandle, params.toBundle());
+        boolean reconfigureCompleted = false;
+        try {
+            reconfigureCompleted = sessionInfo.rangingClosedFuture.get(
+                    RANGE_CTL_TIMEOUT_MILLIS, MILLISECONDS);
+        } catch (InterruptedException | CancellationException | TimeoutException
+                | ExecutionException e) {
+        }
+        if (!reconfigureCompleted) {
+            pw.println("Failed to reconfigure ranging session. Aborting!");
+            return;
+        }
+        pw.println("Ranging session reconfigured");
+    }
+
+    @Override
+    public int onCommand(String cmd) {
+        // Treat no command as help command.
+        if (cmd == null || cmd.equals("")) {
+            cmd = "help";
+        }
+        // Explicit exclusion from root permission
+        if (ArrayUtils.indexOf(NON_PRIVILEGED_COMMANDS, cmd) == -1) {
+            final int uid = Binder.getCallingUid();
+            if (uid != Process.ROOT_UID) {
+                throw new SecurityException(
+                        "Uid " + uid + " does not have access to " + cmd + " uwb command "
+                                + "(or such command doesn't exist)");
+            }
+        }
+
+        final PrintWriter pw = getOutPrintWriter();
+        try {
+            switch (cmd) {
+                case "force-country-code": {
+                    boolean enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled");
+                    if (enabled) {
+                        String countryCode = getNextArgRequired();
+                        if (!UwbCountryCode.isValid(countryCode)) {
+                            pw.println("Invalid argument: Country code must be a 2-Character"
+                                    + " alphanumeric code. But got countryCode " + countryCode
+                                    + " instead");
+                            return -1;
+                        }
+                        mUwbCountryCode.setOverrideCountryCode(countryCode);
+                        return 0;
+                    } else {
+                        mUwbCountryCode.clearOverrideCountryCode();
+                        return 0;
+                    }
+                }
+                case "get-country-code":
+                    pw.println("Uwb Country Code = " + mUwbCountryCode.getCountryCode());
+                    return 0;
+                case "status":
+                    printStatus(pw);
+                    return 0;
+                case "enable-uwb":
+                    mUwbService.setEnabled(true);
+                    return 0;
+                case "disable-uwb":
+                    mUwbService.setEnabled(false);
+                    return 0;
+                case "start-fira-ranging-session":
+                    startFiraRangingSession(pw);
+                    return 0;
+                case "start-ccc-ranging-session":
+                    startCccRangingSession(pw);
+                    return 0;
+                case "reconfigure-fira-ranging-session":
+                    reconfigureFiraRangingSession(pw);
+                    return 0;
+                case "get-ranging-session-reports": {
+                    int sessionId = Integer.parseInt(getNextArgRequired());
+                    SessionInfo sessionInfo = sSessionIdToInfo.get(sessionId);
+                    if (sessionInfo == null) {
+                        pw.println("No active session with session ID: " + sessionId + " found");
+                        return -1;
+                    }
+                    pw.println("Last Ranging results:");
+                    for (RangingReport rangingReport : sessionInfo.lastRangingReports) {
+                        pw.println(rangingReport);
+                    }
+                    return 0;
+                }
+                case "get-all-ranging-session-reports": {
+                    for (SessionInfo sessionInfo: sSessionIdToInfo.values()) {
+                        pw.println("Last Ranging results for sessionId " + sessionInfo.sessionId
+                                + ":");
+                        for (RangingReport rangingReport : sessionInfo.lastRangingReports) {
+                            pw.println(rangingReport);
+                        }
+                    }
+                    return 0;
+                }
+                case "stop-ranging-session":
+                    stopRangingSession(pw);
+                    return 0;
+                case "stop-all-ranging-sessions": {
+                    for (int sessionId : sSessionIdToInfo.keySet()) {
+                        stopRangingSession(pw, sessionId);
+                    }
+                    return 0;
+                }
+                case "get-specification-info": {
+                    PersistableBundle bundle = mUwbService.getSpecificationInfo(null);
+                    pw.println("Specification info: " + bundleToString(bundle));
+                    return 0;
+                }
+                case "get-power-stats": {
+                    PersistableBundle bundle = mUwbService.getSpecificationInfo(null);
+                    GenericSpecificationParams params =
+                            GenericSpecificationParams.fromBundle(bundle);
+                    if (params == null) {
+                        pw.println("Spec info is empty");
+                        return -1;
+                    }
+                    if (params.hasPowerStatsSupport()) {
+                        pw.println(mNativeUwbManager.getPowerStats());
+                    } else {
+                        pw.println("power stats query is not supported");
+                    }
+                    return 0;
+                }
+                default:
+                    return handleDefaultCommands(cmd);
+            }
+        } catch (IllegalArgumentException e) {
+            pw.println("Invalid args for " + cmd + ": ");
+            e.printStackTrace(pw);
+            return -1;
+        } catch (Exception e) {
+            pw.println("Exception while executing UwbShellCommand" + cmd + ": ");
+            e.printStackTrace(pw);
+            return -1;
+        }
+    }
+
+    private boolean getNextArgRequiredTrueOrFalse(String trueString, String falseString)
+            throws IllegalArgumentException {
+        String nextArg = getNextArgRequired();
+        if (trueString.equals(nextArg)) {
+            return true;
+        } else if (falseString.equals(nextArg)) {
+            return false;
+        } else {
+            throw new IllegalArgumentException("Expected '" + trueString + "' or '" + falseString
+                    + "' as next arg but got '" + nextArg + "'");
+        }
+    }
+
+    private void printStatus(PrintWriter pw) throws RemoteException {
+        boolean uwbEnabled =
+                mUwbService.getAdapterState() != UwbManager.AdapterStateCallback.STATE_DISABLED;
+        pw.println("Uwb is " + (uwbEnabled ? "enabled" : "disabled"));
+    }
+
+    private void onHelpNonPrivileged(PrintWriter pw) {
+        pw.println("  status");
+        pw.println("    Gets status of UWB stack");
+        pw.println("  get-country-code");
+        pw.println("    Gets country code as a two-letter string");
+        pw.println("  enable-uwb");
+        pw.println("    Toggle UWB on");
+        pw.println("  disable-uwb");
+        pw.println("    Toggle UWB off");
+        pw.println("  start-fira-ranging-session"
+                + " [-b](blocking call)"
+                + " [-i <sessionId>](session-id)"
+                + " [-c <channel>](channel)"
+                + " [-t controller|controlee](device-type)"
+                + " [-r initiator|responder](device-role)"
+                + " [-a <deviceAddress>](device-address)"
+                + " [-d <destAddress-1, destAddress-2,...>](dest-addresses)"
+                + " [-u ds-twr|ss-twr|ds-twr-non-deferred|ss-twr-non-deferred](round-usage)"
+                + " [-z <numRangeMrmts, numAoaAzimuthMrmts, numAoaElevationMrmts>"
+                + "(interleaving-ratio)"
+                + " [-e none|enabled|azimuth-only|elevation-only](aoa type)");
+        pw.println("    Starts a FIRA ranging session with the provided params."
+                + " Note: default behavior is to cache the latest ranging reports which can be"
+                + " retrieved using |get-ranging-session-reports|");
+        pw.println("  start-ccc-ranging-session"
+                + " [-b](blocking call)"
+                + " Ranging reports will be displayed on screen)"
+                + " [-u 0|1](uwb-config)"
+                + " [-p <tx>,<rx>](pulse-shape-combo)"
+                + " [-i <sessionId>](session-id)"
+                + " [-r <ran_multiplier>](ran-multiplier)"
+                + " [-c <channel>](channel)"
+                + " [-p <num-chaps-per-slot>](num-chaps-per-slot)"
+                + " [-n <num-responder-nodes>](num-responder-nodes)"
+                + " [-o <num-slots-per-round>](num-slots-per-round)"
+                + " [-s <sync-code-index>](sync-code-index)"
+                + " [-h none|continuous|adaptive](hopping-config-mode)"
+                + " [-a default|aes](hopping-sequence)");
+        pw.println("    Starts a CCC ranging session with the provided params."
+                + " Note: default behavior is to cache the latest ranging reports which can be"
+                + " retrieved using |get-ranging-session-reports|");
+        pw.println("  reconfigure-fira-ranging-session"
+                + " <sessionId>"
+                + " [-a add|delete](action)"
+                + " [-d <destAddress-1, destAddress-2,...>](dest-addresses)"
+                + " [-s <subSessionId-1, subSessionId-2,...>](sub-sessionIds)");
+        pw.println("  get-ranging-session-reports <sessionId>");
+        pw.println("    Displays latest cached ranging reports for an ongoing ranging session");
+        pw.println("  get-all-ranging-session-reports");
+        pw.println("    Displays latest cached ranging reports for all ongoing ranging session");
+        pw.println("  stop-ranging-session <sessionId>");
+        pw.println("    Stops an ongoing ranging session");
+        pw.println("  stop-all-ranging-sessions");
+        pw.println("    Stops all ongoing ranging sessions");
+        pw.println("  get-specification-info");
+        pw.println("    Gets specification info from uwb chip");
+    }
+
+    private void onHelpPrivileged(PrintWriter pw) {
+        pw.println("  force-country-code enabled <two-letter code> | disabled ");
+        pw.println("    Sets country code to <two-letter code> or left for normal value");
+        pw.println("  get-power-stats");
+        pw.println("    Get power stats");
+    }
+
+    @Override
+    public void onHelp() {
+        final PrintWriter pw = getOutPrintWriter();
+        pw.println("UWB (ultra wide-band) commands:");
+        pw.println("  help or -h");
+        pw.println("    Print this help text.");
+        onHelpNonPrivileged(pw);
+        if (Binder.getCallingUid() == Process.ROOT_UID) {
+            onHelpPrivileged(pw);
+        }
+        pw.println();
+    }
+
+    @VisibleForTesting
+    public void reset() {
+        sSessionHandleIdNext = 0;
+        sSessionIdToInfo.clear();
+    }
+}
diff --git a/service/java/com/android/server/uwb/UwbTestUtils.java b/service/java/com/android/server/uwb/UwbTestUtils.java
new file mode 100644
index 0000000..a9a216a
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbTestUtils.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 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.
+ */
+
+package com.android.server.uwb;
+
+import static com.android.server.uwb.data.UwbUciConstants.RANGING_MEASUREMENT_TYPE_TWO_WAY;
+import static com.android.server.uwb.util.UwbUtil.convertFloatToQFormat;
+import static com.android.server.uwb.util.UwbUtil.degreeToRadian;
+
+import android.util.Pair;
+import android.uwb.AngleMeasurement;
+import android.uwb.AngleOfArrivalMeasurement;
+import android.uwb.DistanceMeasurement;
+import android.uwb.RangingMeasurement;
+import android.uwb.RangingReport;
+import android.uwb.UwbAddress;
+
+import com.android.server.uwb.data.UwbRangingData;
+import com.android.server.uwb.data.UwbTwoWayMeasurement;
+import com.android.server.uwb.params.TlvUtil;
+
+import com.google.uwb.support.fira.FiraParams;
+
+public class UwbTestUtils {
+    private static final long TEST_SEQ_COUNTER = 5;
+    private static final long TEST_SESSION_ID = 7;
+    private static final int TEST_RCR_INDICATION = 7;
+    private static final long TEST_CURR_RANGING_INTERVAL = 100;
+    private static final int TEST_RANGING_MEASURES_TYPE = RANGING_MEASUREMENT_TYPE_TWO_WAY;
+    private static final int TEST_MAC_ADDRESS_MODE = 1;
+    private static final byte[] TEST_MAC_ADDRESS = {0x1, 0x3};
+    private static final int TEST_STATUS = FiraParams.STATUS_CODE_OK;
+    private static final int TEST_LOS = 0;
+    private static final int TEST_DISTANCE = 101;
+    private static final float TEST_AOA_AZIMUTH = 67;
+    private static final int TEST_AOA_AZIMUTH_FOM = 50;
+    private static final float TEST_AOA_ELEVATION = 37;
+    private static final int TEST_AOA_ELEVATION_FOM = 90;
+    private static final float TEST_AOA_DEST_AZIMUTH = 67;
+    private static final int TEST_AOA_DEST_AZIMUTH_FOM = 50;
+    private static final float TEST_AOA_DEST_ELEVATION = 37;
+    private static final int TEST_AOA_DEST_ELEVATION_FOM = 90;
+    private static final int TEST_SLOT_IDX = 10;
+
+    private UwbTestUtils() {}
+
+    public static UwbRangingData generateRangingData(int rangingStatus) {
+        final int noOfRangingMeasures = 1;
+        final UwbTwoWayMeasurement[] uwbTwoWayMeasurements =
+                new UwbTwoWayMeasurement[noOfRangingMeasures];
+        uwbTwoWayMeasurements[0] = new UwbTwoWayMeasurement(TEST_MAC_ADDRESS, rangingStatus,
+                TEST_LOS, TEST_DISTANCE, convertFloatToQFormat(TEST_AOA_AZIMUTH, 9, 7),
+                TEST_AOA_AZIMUTH_FOM, convertFloatToQFormat(TEST_AOA_ELEVATION, 9, 7),
+                TEST_AOA_ELEVATION_FOM, convertFloatToQFormat(TEST_AOA_DEST_AZIMUTH, 9, 7),
+                TEST_AOA_DEST_AZIMUTH_FOM, convertFloatToQFormat(TEST_AOA_DEST_ELEVATION, 9, 7),
+                TEST_AOA_DEST_ELEVATION_FOM, TEST_SLOT_IDX);
+        return new UwbRangingData(TEST_SEQ_COUNTER, TEST_SESSION_ID,
+                TEST_RCR_INDICATION, TEST_CURR_RANGING_INTERVAL, TEST_RANGING_MEASURES_TYPE,
+                TEST_MAC_ADDRESS_MODE, noOfRangingMeasures, uwbTwoWayMeasurements);
+    }
+
+    // Helper method to generate a UwbRangingData instance and corresponding RangingMeasurement
+    public static Pair<UwbRangingData, RangingReport> generateRangingDataAndRangingReport(
+            boolean isAoaAzimuthEnabled, boolean isAoaElevationEnabled,
+            boolean isDestAoaAzimuthEnabled, boolean isDestAoaElevationEnabled,
+            long elapsedRealtimeNanos) {
+        UwbRangingData uwbRangingData = generateRangingData(TEST_STATUS);
+
+        AngleOfArrivalMeasurement aoaMeasurement = null;
+        AngleOfArrivalMeasurement aoaDestMeasurement = null;
+        if (isAoaAzimuthEnabled || isAoaElevationEnabled) {
+            AngleMeasurement aoaAzimuth = null;
+            AngleMeasurement aoaElevation = null;
+            if (isAoaAzimuthEnabled) {
+                aoaAzimuth =
+                        new AngleMeasurement(
+                                degreeToRadian(TEST_AOA_AZIMUTH), 0,
+                                TEST_AOA_AZIMUTH_FOM / (double) 100);
+            }
+            if (isAoaElevationEnabled) {
+                aoaElevation =
+                        new AngleMeasurement(
+                                degreeToRadian(TEST_AOA_ELEVATION), 0,
+                                TEST_AOA_ELEVATION_FOM / (double) 100);
+            }
+            aoaMeasurement = new AngleOfArrivalMeasurement.Builder(aoaAzimuth)
+                    .setAltitude(aoaElevation)
+                    .build();
+        }
+        if (isDestAoaAzimuthEnabled || isDestAoaElevationEnabled) {
+            AngleMeasurement aoaDestAzimuth = null;
+            AngleMeasurement aoaDestElevation = null;
+            if (isDestAoaAzimuthEnabled) {
+                aoaDestAzimuth =
+                        new AngleMeasurement(
+                                degreeToRadian(TEST_AOA_DEST_AZIMUTH), 0,
+                                TEST_AOA_DEST_AZIMUTH_FOM / (double) 100);
+            }
+            if (isDestAoaElevationEnabled) {
+                aoaDestElevation =
+                        new AngleMeasurement(
+                                degreeToRadian(TEST_AOA_DEST_ELEVATION), 0,
+                                TEST_AOA_DEST_ELEVATION_FOM / (double) 100);
+            }
+            aoaDestMeasurement = new AngleOfArrivalMeasurement.Builder(aoaDestAzimuth)
+                    .setAltitude(aoaDestElevation)
+                    .build();
+        }
+        RangingMeasurement rangingMeasurement = new RangingMeasurement.Builder()
+                .setRemoteDeviceAddress(UwbAddress.fromBytes(
+                        TlvUtil.getReverseBytes(TEST_MAC_ADDRESS)))
+                .setStatus(TEST_STATUS)
+                .setElapsedRealtimeNanos(elapsedRealtimeNanos)
+                .setDistanceMeasurement(
+                        new DistanceMeasurement.Builder()
+                                .setMeters(TEST_DISTANCE / (double) 100)
+                                .setErrorMeters(0)
+                                .setConfidenceLevel(0)
+                                .build())
+                .setAngleOfArrivalMeasurement(aoaMeasurement)
+                .setDestinationAngleOfArrivalMeasurement(aoaDestMeasurement)
+                .setLineOfSight(TEST_LOS)
+                .build();
+        RangingReport rangingReport = new RangingReport.Builder()
+                .addMeasurement(rangingMeasurement)
+                .build();
+        return Pair.create(uwbRangingData, rangingReport);
+    }
+}
diff --git a/service/java/com/android/server/uwb/config/CapabilityParam.java b/service/java/com/android/server/uwb/config/CapabilityParam.java
new file mode 100644
index 0000000..fe59912
--- /dev/null
+++ b/service/java/com/android/server/uwb/config/CapabilityParam.java
@@ -0,0 +1,148 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.config;
+
+import android.hardware.uwb.fira_android.UwbVendorCapabilityTlvTypes;
+import android.hardware.uwb.fira_android.UwbVendorCapabilityTlvValues;
+
+public class CapabilityParam {
+    /**
+     *  CR 287 params
+     */
+    public static final int SUPPORTED_FIRA_PHY_VERSION_RANGE = 0x0;
+    public static final int SUPPORTED_FIRA_MAC_VERSION_RANGE = 0x1;
+    public static final int SUPPORTED_DEVICE_ROLES = 0x2;
+    public static final int SUPPORTED_RANGING_METHOD = 0x3;
+    public static final int SUPPORTED_STS_CONFIG = 0x4;
+    public static final int SUPPORTED_MULTI_NODE_MODES = 0x5;
+    public static final int SUPPORTED_RANGING_TIME_STRUCT = 0x6;
+    public static final int SUPPORTED_SCHEDULED_MODE = 0x7;
+    public static final int SUPPORTED_HOPPING_MODE = 0x8;
+    public static final int SUPPORTED_BLOCK_STRIDING = 0x9;
+    public static final int SUPPORTED_UWB_INITIATION_TIME = 0x0A;
+    public static final int SUPPORTED_CHANNELS = 0x0B;
+    public static final int SUPPORTED_RFRAME_CONFIG = 0x0C;
+    public static final int SUPPORTED_CC_CONSTRAINT_LENGTH = 0x0D;
+    public static final int SUPPORTED_BPRF_PARAMETER_SETS = 0x0E;
+    public static final int SUPPORTED_HPRF_PARAMETER_SETS = 0x0F;
+    public static final int SUPPORTED_AOA = 0x10;
+    public static final int SUPPORTED_EXTENDED_MAC_ADDRESS = 0x11;
+    public static final int SUPPORTED_AOA_RESULT_REQ_INTERLEAVING =
+            UwbVendorCapabilityTlvTypes.SUPPORTED_AOA_RESULT_REQ_ANTENNA_INTERLEAVING;
+
+    // CCC specific
+    public static final int CCC_SUPPORTED_VERSIONS =
+             UwbVendorCapabilityTlvTypes.CCC_SUPPORTED_VERSIONS;
+    public static final int CCC_SUPPORTED_UWB_CONFIGS =
+             UwbVendorCapabilityTlvTypes.CCC_SUPPORTED_UWB_CONFIGS;
+    public static final int CCC_SUPPORTED_PULSE_SHAPE_COMBOS =
+             UwbVendorCapabilityTlvTypes.CCC_SUPPORTED_PULSE_SHAPE_COMBOS;
+    public static final int CCC_SUPPORTED_RAN_MULTIPLIER =
+             UwbVendorCapabilityTlvTypes.CCC_SUPPORTED_RAN_MULTIPLIER;
+    public static final int CCC_SUPPORTED_CHAPS_PER_SLOT =
+             UwbVendorCapabilityTlvTypes.CCC_SUPPORTED_CHAPS_PER_SLOT;
+    public static final int CCC_SUPPORTED_SYNC_CODES =
+             UwbVendorCapabilityTlvTypes.CCC_SUPPORTED_SYNC_CODES;
+    public static final int CCC_SUPPORTED_CHANNELS =
+             UwbVendorCapabilityTlvTypes.CCC_SUPPORTED_CHANNELS;
+    public static final int CCC_SUPPORTED_HOPPING_CONFIG_MODES_AND_SEQUENCES =
+             UwbVendorCapabilityTlvTypes.CCC_SUPPORTED_HOPPING_CONFIG_MODES_AND_SEQUENCES;
+
+    public static final int RESPONDER = 0x01;
+    public static final int INITIATOR = 0x02;
+
+    public static final int OWR = 0x01;
+    public static final int SS_TWR_DEFERRED = 0x02;
+    public static final int DS_TWR_DEFERRED = 0x04;
+    public static final int SS_TWR_NON_DEFERRED = 0x08;
+    public static final int DS_TWR_NON_DEFERRED = 0x10;
+
+    public static final int STATIC_STS = 0x1;
+    public static final int DYNAMIC_STS = 0x2;
+    public static final int DYNAMIC_STS_RESPONDER_SPECIFIC_SUBSESSION_KEY = 0x4;
+
+    public static final int UNICAST = 0x1;
+    public static final int ONE_TO_MANY = 0x2;
+    public static final int MANY_TO_MANY = 0x4;
+
+    public static final int NO_BLOCK_STRIDING = 0x0;
+    public static final int BLOCK_STRIDING = 0x1;
+
+    public static final int NO_UWB_INITIATION_TIME = 0x0;
+    public static final int UWB_INITIATION_TIME = 0x1;
+
+    public static final int CHANNEL_5 = 0x1;
+    public static final int CHANNEL_6 = 0x2;
+    public static final int CHANNEL_8 = 0x4;
+    public static final int CHANNEL_9 = 0x8;
+    public static final int CHANNEL_10 = 0x10;
+    public static final int CHANNEL_12 = 0x20;
+    public static final int CHANNEL_13 = 0x40;
+    public static final int CHANNEL_14 = 0x80;
+
+    public static final int SP0 = 0x1;
+    public static final int SP1 = 0x2;
+    public static final int SP2 = 0x4;
+    public static final int SP3 = 0x8;
+
+    public static final int CC_CONSTRAINT_LENGTH_K3 = 0x1;
+    public static final int CC_CONSTRAINT_LENGTH_K7 = 0x2;
+
+    public static final int AOA_AZIMUTH_90 = 0x1;
+    public static final int AOA_AZIMUTH_180 = 0x2;
+    public static final int AOA_ELEVATION = 0x4;
+    public static final int AOA_FOM = 0x4;
+
+    public static final int NO_EXTENDED_MAC = 0x0;
+    public static final int EXTENDED_MAC = 0x1;
+
+    public static final int NO_AOA_RESULT_REQ_INTERLEAVING = 0x0;
+    public static final int AOA_RESULT_REQ_INTERLEAVING = 0x1;
+
+    public static final int CCC_CHANNEL_5 = (int) UwbVendorCapabilityTlvValues.CCC_CHANNEL_5;
+    public static final int CCC_CHANNEL_9 = (int) UwbVendorCapabilityTlvValues.CCC_CHANNEL_9;
+
+    public static final int CCC_CHAPS_PER_SLOT_3 =
+            (int) UwbVendorCapabilityTlvValues.CHAPS_PER_SLOT_3;
+    public static final int CCC_CHAPS_PER_SLOT_4 =
+            (int) UwbVendorCapabilityTlvValues.CHAPS_PER_SLOT_4;
+    public static final int CCC_CHAPS_PER_SLOT_6 =
+            (int) UwbVendorCapabilityTlvValues.CHAPS_PER_SLOT_6;
+    public static final int CCC_CHAPS_PER_SLOT_8 =
+            (int) UwbVendorCapabilityTlvValues.CHAPS_PER_SLOT_8;
+    public static final int CCC_CHAPS_PER_SLOT_9 =
+            (int) UwbVendorCapabilityTlvValues.CHAPS_PER_SLOT_9;
+    public static final int CCC_CHAPS_PER_SLOT_12 =
+            (int) UwbVendorCapabilityTlvValues.CHAPS_PER_SLOT_12;
+    public static final int CCC_CHAPS_PER_SLOT_24 =
+            (int) UwbVendorCapabilityTlvValues.CHAPS_PER_SLOT_24;
+
+    public static final int CCC_HOPPING_CONFIG_MODE_NONE =
+            (int) UwbVendorCapabilityTlvValues.HOPPING_CONFIG_MODE_NONE;
+    public static final int CCC_HOPPING_CONFIG_MODE_CONTINUOUS =
+            (int) UwbVendorCapabilityTlvValues.HOPPING_CONFIG_MODE_CONTINUOUS;
+    public static final int CCC_HOPPING_CONFIG_MODE_ADAPTIVE =
+            (int) UwbVendorCapabilityTlvValues.HOPPING_CONFIG_MODE_ADAPTIVE;
+
+    public static final int CCC_HOPPING_SEQUENCE_AES =
+            (int) UwbVendorCapabilityTlvValues.HOPPING_SEQUENCE_AES;
+    public static final int CCC_HOPPING_SEQUENCE_DEFAULT =
+            (int) UwbVendorCapabilityTlvValues.HOPPING_SEQUENCE_DEFAULT;
+
+    public static final int SUPPORTED_POWER_STATS_QUERY =
+            UwbVendorCapabilityTlvTypes.SUPPORTED_POWER_STATS_QUERY;
+}
diff --git a/service/java/com/android/server/uwb/config/ConfigParam.java b/service/java/com/android/server/uwb/config/ConfigParam.java
new file mode 100644
index 0000000..580933d
--- /dev/null
+++ b/service/java/com/android/server/uwb/config/ConfigParam.java
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.config;
+
+import static android.hardware.uwb.fira_android.UwbVendorSessionAppConfigTlvTypes.CCC_HOP_MODE_KEY;
+import static android.hardware.uwb.fira_android.UwbVendorSessionAppConfigTlvTypes.CCC_PULSESHAPE_COMBO;
+import static android.hardware.uwb.fira_android.UwbVendorSessionAppConfigTlvTypes.CCC_RANGING_PROTOCOL_VER;
+import static android.hardware.uwb.fira_android.UwbVendorSessionAppConfigTlvTypes.CCC_URSK_TTL;
+import static android.hardware.uwb.fira_android.UwbVendorSessionAppConfigTlvTypes.CCC_UWB_CONFIG_ID;
+import static android.hardware.uwb.fira_android.UwbVendorSessionAppConfigTlvTypes.CCC_UWB_TIME0;
+import static android.hardware.uwb.fira_android.UwbVendorSessionAppConfigTlvTypes.NB_OF_AZIMUTH_MEASUREMENTS;
+import static android.hardware.uwb.fira_android.UwbVendorSessionAppConfigTlvTypes.NB_OF_ELEVATION_MEASUREMENTS;
+import static android.hardware.uwb.fira_android.UwbVendorSessionAppConfigTlvTypes.NB_OF_RANGE_MEASUREMENTS;
+import static android.hardware.uwb.fira_android.UwbVendorSessionAppConfigTlvValues.AOA_RESULT_REQ_ANTENNA_INTERLEAVING;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+public class ConfigParam {
+
+    /**
+     * App Config Parameter ID's
+     **/
+    public static final int DEVICE_TYPE = 0x00;
+    public static final int RANGING_ROUND_USAGE = 0X01;
+    public static final int STS_CONFIG = 0X02;
+    public static final int MULTI_NODE_MODE = 0X03;
+    public static final int CHANNEL_NUMBER = 0x04;
+    public static final int NUMBER_OF_CONTROLEES = 0x05;
+    public static final int DEVICE_MAC_ADDRESS = 0x06;
+    public static final int DST_MAC_ADDRESS = 0x07;
+    public static final int SLOT_DURATION = 0x08;
+    public static final int RANGING_INTERVAL = 0x09;
+    public static final int STS_INDEX = 0x0A;
+    public static final int MAC_FCS_TYPE = 0x0B;
+    public static final int RANGING_ROUND_CONTROL = 0x0C;
+    public static final int AOA_RESULT_REQ = 0x0D;
+    public static final int RANGE_DATA_NTF_CONFIG = 0x0E;
+    public static final int RANGE_DATA_NTF_PROXIMITY_NEAR = 0x0F;
+    public static final int RANGE_DATA_NTF_PROXIMITY_FAR = 0x10;
+    public static final int DEVICE_ROLE = 0x11;
+    public static final int RFRAME_CONFIG = 0x12;
+    public static final int PREAMBLE_CODE_INDEX = 0x14;
+    public static final int SFD_ID = 0x15;
+    public static final int PSDU_DATA_RATE = 0x16;
+    public static final int PREAMBLE_DURATION = 0x17;
+    public static final int RANGING_TIME_STRUCT = 0x1A;
+    public static final int SLOTS_PER_RR = 0x1B;
+    public static final int TX_ADAPTIVE_PAYLOAD_POWER = 0x1C;
+    //public static final int TX_ANTENNA_SELECTION = 0x1D;
+    public static final int RESPONDER_SLOT_INDEX = 0x1E;
+    public static final int PRF_MODE = 0x1F;
+    public static final int SCHEDULED_MODE = 0x22;
+    public static final int KEY_ROTATION = 0x23;
+    public static final int KEY_ROTATION_RATE = 0x24;
+    public static final int SESSION_PRIORITY = 0x25;
+    public static final int MAC_ADDRESS_MODE = 0x26;
+    public static final int VENDOR_ID = 0x27;
+    public static final int STATIC_STS_IV = 0x28;
+    public static final int NUMBER_OF_STS_SEGMENTS = 0x29;
+    public static final int MAX_RR_RETRY = 0x2A;
+    public static final int UWB_INITIATION_TIME = 0x2B;
+    public static final int HOPPING_MODE = 0x2C;
+    public static final int BLOCK_STRIDE_LENGTH = 0x2D;
+    public static final int RESULT_REPORT_CONFIG = 0x2E;
+    public static final int IN_BAND_TERMINATION_ATTEMPT_COUNT = 0x2F;
+    public static final int SUB_SESSION_ID = 0x30;
+    public static final int BPRF_PHR_DATA_RATE = 0x31;
+    public static final int MAX_NUMBER_OF_MEASUREMENTS = 0x32;
+    public static final int STS_LENGTH = 0x35;
+    public static final int NUM_RANGE_MEASUREMENTS = NB_OF_RANGE_MEASUREMENTS;
+    public static final int NUM_AOA_AZIMUTH_MEASUREMENTS = NB_OF_AZIMUTH_MEASUREMENTS;
+    public static final int NUM_AOA_ELEVATION_MEASUREMENTS = NB_OF_ELEVATION_MEASUREMENTS;
+
+    public static final int VENDOR_ID_BYTE_COUNT = 2;
+    public static final int STATIC_STS_IV_BYTE_COUNT = 6;
+    public static final int AOA_RESULT_REQ_INTERLEAVING = AOA_RESULT_REQ_ANTENNA_INTERLEAVING;
+
+    // CCC
+    //OpenParams
+    public static final int RANGING_PROTOCOL_VER = CCC_RANGING_PROTOCOL_VER;
+    public static final int UWB_CONFIG_ID = CCC_UWB_CONFIG_ID;
+    public static final int PULSESHAPE_COMBO = CCC_PULSESHAPE_COMBO;
+    public static final int URSK_TTL = CCC_URSK_TTL;
+    //StartedParams
+    public static final int HOP_MODE_KEY = CCC_HOP_MODE_KEY;
+    public static final int HOP_MODE_KEY_BYTE = 16;
+    public static final int UWB_TIME0 = CCC_UWB_TIME0;
+
+    public static final int RANGING_PROTOCOL_VER_BYTE_COUNT = 2;
+
+    public static byte[] getTagBytes(int tagType) {
+        int tagLength = 1;
+        ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES).putInt(tagType);
+        return Arrays.copyOfRange(buffer.array(), Integer.BYTES - tagLength, Integer.BYTES);
+    }
+}
diff --git a/service/java/com/android/server/uwb/data/UwbCccConstants.java b/service/java/com/android/server/uwb/data/UwbCccConstants.java
new file mode 100644
index 0000000..9deffac
--- /dev/null
+++ b/service/java/com/android/server/uwb/data/UwbCccConstants.java
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.data;
+
+public class UwbCccConstants {
+
+    /* CCC Hopping [S]*/
+    public static final int HOPPING_CONFIG_MODE_NONE = 0X00;
+    public static final int HOPPING_CONFIG_MODE_CONTINUOUS_DEFAULT = 0X03;
+    public static final int HOPPING_CONFIG_MODE_CONTINUOUS_AES = 0X05;
+
+    public static final int HOPPING_CONFIG_MODE_MODE_ADAPTIVE_DEFAULT = 0X02;
+    public static final int HOPPING_CONFIG_MODE_MODE_ADAPTIVE_AES = 0X04;
+
+
+    public static final String KEY_STARTING_STS_INDEX = "starting_sts_index";
+    public static final String KEY_UWB_TIME_0 = "uwb_time_0";
+    public static final String KEY_HOP_MODE_KEY = "hop_mode_key";
+    public static final String KEY_SYNC_CODE_INDEX = "sync_code_index";
+    public static final String KEY_RAN_MULTIPLIER = "ran_multiplier";
+    /* CCC Hopping [E]*/
+}
diff --git a/service/java/com/android/server/uwb/data/UwbConfigStatusData.java b/service/java/com/android/server/uwb/data/UwbConfigStatusData.java
new file mode 100644
index 0000000..1dd0f47
--- /dev/null
+++ b/service/java/com/android/server/uwb/data/UwbConfigStatusData.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.data;
+
+import java.util.Arrays;
+
+public class UwbConfigStatusData {
+    public final int status;
+    public final int length;
+    public final byte[] cgfStatus;
+
+    public UwbConfigStatusData(int status, int length, byte[] cgfStatus) {
+        this.status = status;
+        this.length = length;
+        this.cgfStatus = cgfStatus;
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    public int getLength() {
+        return length;
+    }
+
+    public byte[] getCfgStatus() {
+        return cgfStatus;
+    }
+
+    @Override
+    public String toString() {
+        return "UwbConfigStatusData { "
+                + " status = " + status
+                + " length = " + length
+                + ", tlvs = [" + Arrays.toString(cgfStatus)
+                + "] }";
+    }
+}
diff --git a/service/java/com/android/server/uwb/data/UwbMulticastListUpdateStatus.java b/service/java/com/android/server/uwb/data/UwbMulticastListUpdateStatus.java
new file mode 100644
index 0000000..14c368c
--- /dev/null
+++ b/service/java/com/android/server/uwb/data/UwbMulticastListUpdateStatus.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.data;
+import java.util.Arrays;
+
+public class UwbMulticastListUpdateStatus {
+    private long mSessionId;
+    private int mRemainingSize;
+    private int mNumOfControlees;
+    private int [] mContolleeMacAddress;
+    private long[] mSubSessionId;
+    private int[] mStatus;
+
+    public UwbMulticastListUpdateStatus(long sessionID, int remainingSize, int numOfControlees,
+            int[] contolleeMacAddress, long[] subSessionId, int[] status) {
+        this.mSessionId = sessionID;
+        this.mRemainingSize = remainingSize;
+        this.mNumOfControlees = numOfControlees;
+        this.mContolleeMacAddress = contolleeMacAddress;
+        this.mSubSessionId = subSessionId;
+        this.mStatus = status;
+    }
+
+    public long getSessionId() {
+        return mSessionId;
+    }
+
+    public int getRemainingSize() {
+        return mRemainingSize;
+    }
+
+    public int getNumOfControlee() {
+        return mNumOfControlees;
+    }
+
+    public int[] getContolleeMacAddress() {
+        return mContolleeMacAddress;
+    }
+
+    public long[] getSubSessionId() {
+        return mSubSessionId;
+    }
+
+    public int[] getStatus() {
+        return mStatus;
+    }
+
+    @Override
+    public String toString() {
+        return "UwbMulticastListUpdateEvent { "
+                + " SessionID =" + mSessionId
+                + ", RemainingSize =" + mRemainingSize
+                + ", NumOfControlee =" + mNumOfControlees
+                + ", MacAddress =" + Arrays.toString(mContolleeMacAddress)
+                + ", SubSessionId =" + Arrays.toString(mSubSessionId)
+                + ", Status =" + Arrays.toString(mStatus)
+                + '}';
+    }
+}
diff --git a/service/java/com/android/server/uwb/data/UwbRangingData.java b/service/java/com/android/server/uwb/data/UwbRangingData.java
new file mode 100644
index 0000000..44be8d5
--- /dev/null
+++ b/service/java/com/android/server/uwb/data/UwbRangingData.java
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.data;
+
+import java.util.Arrays;
+
+public class UwbRangingData {
+    public long mSeqCounter;
+    public long mSessionId;
+    public int mRcrIndication;
+    public long mCurrRangingInterval;
+    public int mRangingMeasuresType;
+    public int mMacAddressMode;
+    public int mNoOfRangingMeasures;
+    public UwbTwoWayMeasurement[] mRangingTwoWayMeasures;
+
+    public UwbRangingData(long seqCounter, long sessionId, int rcrIndication,
+            long currRangingInterval, int rangingMeasuresType, int macAddressMode,
+            int noOfRangingMeasures, UwbTwoWayMeasurement[] rangingTwoWayMeasures) {
+        this.mSeqCounter = seqCounter;
+        this.mSessionId = sessionId;
+        this.mRcrIndication = rcrIndication;
+        this.mCurrRangingInterval = currRangingInterval;
+        this.mRangingMeasuresType = rangingMeasuresType;
+        this.mMacAddressMode = macAddressMode;
+        this.mNoOfRangingMeasures = noOfRangingMeasures;
+        this.mRangingTwoWayMeasures = rangingTwoWayMeasures;
+    }
+
+    public long getSequenceCounter() {
+        return mSeqCounter;
+    }
+
+    public long getSessionId() {
+        return mSessionId;
+    }
+
+    public int getRcrIndication() {
+        return mRcrIndication;
+    }
+
+    public long getCurrRangingInterval() {
+        return mCurrRangingInterval;
+    }
+
+    public int getRangingMeasuresType() {
+        return mRangingMeasuresType;
+    }
+
+    public int getMacAddressMode() {
+        return mMacAddressMode;
+    }
+
+    public int getNoOfRangingMeasures() {
+        return mNoOfRangingMeasures;
+    }
+
+    public UwbTwoWayMeasurement[] getRangingTwoWayMeasures() {
+        return mRangingTwoWayMeasures;
+    }
+
+    public String toString() {
+        if (mRangingMeasuresType == UwbUciConstants.RANGING_MEASUREMENT_TYPE_TWO_WAY) {
+            return "UwbRangingData { "
+                    + " SeqCounter = " + mSeqCounter
+                    + ", SessionId = " + mSessionId
+                    + ", RcrIndication = " + mRcrIndication
+                    + ", CurrRangingInterval = " + mCurrRangingInterval
+                    + ", RangingMeasuresType = " + mRangingMeasuresType
+                    + ", MacAddressMode = " + mMacAddressMode
+                    + ", NoOfRangingMeasures = " + mNoOfRangingMeasures
+                    + ", RangingTwoWayMeasures = " + Arrays.toString(mRangingTwoWayMeasures)
+                    + '}';
+        } else {
+            // TODO(jh0.jang) : ONE WAY RANGING(TDOA)?
+            return null;
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/data/UwbTlvData.java b/service/java/com/android/server/uwb/data/UwbTlvData.java
new file mode 100644
index 0000000..551fecb
--- /dev/null
+++ b/service/java/com/android/server/uwb/data/UwbTlvData.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.data;
+
+import java.util.Arrays;
+
+public class UwbTlvData {
+    public final int status;
+    public final int length;
+    public final byte[] tlvs;
+
+    public UwbTlvData(int status, int length, byte[] tlvs) {
+        this.status = status;
+        this.length = length;
+        this.tlvs = tlvs;
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    public int getLength() {
+        return length;
+    }
+
+    public byte[] getTlv() {
+        return tlvs;
+    }
+
+    @Override
+    public String toString() {
+        return "UwbTlvData { "
+                + " status = " + status
+                + " length = " + length
+                + ", tlvs = [" + Arrays.toString(tlvs)
+                + "] }";
+    }
+}
diff --git a/service/java/com/android/server/uwb/data/UwbTwoWayMeasurement.java b/service/java/com/android/server/uwb/data/UwbTwoWayMeasurement.java
new file mode 100644
index 0000000..089b692
--- /dev/null
+++ b/service/java/com/android/server/uwb/data/UwbTwoWayMeasurement.java
@@ -0,0 +1,129 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.data;
+
+import com.android.server.uwb.util.UwbUtil;
+
+public class UwbTwoWayMeasurement {
+    public byte[] mMacAddress;
+    public int mStatus;
+    public int mNLoS;
+    public int mDistance;
+    public float mAoaAzimuth;
+    public int mAoaAzimuthFom;
+    public float mAoaElevation;
+    public int mAoaElevationFom;
+    public float mAoaDestAzimuth;
+    public int mAoaDestAzimuthFom;
+    public float mAoaDestElevation;
+    public int mAoaDestElevationFom;
+    public int mSlotIndex;
+
+    public UwbTwoWayMeasurement(byte[] macAddress, int status, int nLoS, int distance,
+            int aoaAzimuth, int aoaAzimuthFom, int aoaElevation,
+            int aoaElevationFom, int aoaDestAzimuth, int aoaDestAzimuthFom,
+            int aoaDestElevation, int aoaDestElevationFom, int slotIndex) {
+
+        this.mMacAddress = macAddress;
+        this.mStatus = status;
+        this.mNLoS = nLoS;
+        this.mDistance = distance;
+        this.mAoaAzimuth = toFloatFromQFormat(aoaAzimuth);
+        this.mAoaAzimuthFom = aoaAzimuthFom;
+        this.mAoaElevation = toFloatFromQFormat(aoaElevation);
+        this.mAoaElevationFom = aoaElevationFom;
+        this.mAoaDestAzimuth = toFloatFromQFormat(aoaDestAzimuth);
+        this.mAoaDestAzimuthFom = aoaDestAzimuthFom;
+        this.mAoaDestElevation = toFloatFromQFormat(aoaDestElevation);
+        this.mAoaDestElevationFom = aoaDestElevationFom;
+        this.mSlotIndex = slotIndex;
+    }
+
+    public byte[] getMacAddress() {
+        return mMacAddress;
+    }
+
+    public int getRangingStatus() {
+        return mStatus;
+    }
+
+    public int getNLoS() {
+        return mNLoS;
+    }
+
+    public int getDistance() {
+        return mDistance;
+    }
+
+    public float getAoaAzimuth() {
+        return mAoaAzimuth;
+    }
+
+    public int getAoaAzimuthFom() {
+        return mAoaAzimuthFom;
+    }
+
+    public float getAoaElevation() {
+        return mAoaElevation;
+    }
+
+    public int getAoaElevationFom() {
+        return mAoaElevationFom;
+    }
+
+    public float getAoaDestAzimuth() {
+        return mAoaDestAzimuth;
+    }
+
+    public int getAoaDestAzimuthFom() {
+        return mAoaDestAzimuthFom;
+    }
+
+    public float getAoaDestElevation() {
+        return mAoaDestElevation;
+    }
+
+    public int getAoaDestElevationFom() {
+        return mAoaDestElevationFom;
+    }
+
+    public int getSlotIndex() {
+        return mSlotIndex;
+    }
+
+    private float toFloatFromQFormat(int value) {
+        return UwbUtil.convertQFormatToFloat(UwbUtil.twos_compliment(value, 16),
+                9, 7);
+    }
+
+    public String toString() {
+        return "UwbTwoWayMeasurement { "
+                + " MacAddress = " + UwbUtil.toHexString(mMacAddress)
+                + ", RangingStatus = " + mStatus
+                + ", NLoS = " + mNLoS
+                + ", Distance = " + mDistance
+                + ", AoaAzimuth = " + mAoaAzimuth
+                + ", AoaAzimuthFom = " + mAoaAzimuthFom
+                + ", AoaElevation = " + mAoaElevation
+                + ", AoaElevationFom = " + mAoaElevationFom
+                + ", AoaDestAzimuth = " + mAoaDestAzimuth
+                + ", AoaDestAzimuthFom = " + mAoaDestAzimuthFom
+                + ", AoaDestElevation = " + mAoaDestElevation
+                + ", AoaDestElevationFom = " + mAoaDestElevationFom
+                + ", SlotIndex = 0x" + UwbUtil.toHexString(mSlotIndex)
+                + '}';
+    }
+}
diff --git a/service/java/com/android/server/uwb/data/UwbUciConstants.java b/service/java/com/android/server/uwb/data/UwbUciConstants.java
new file mode 100644
index 0000000..2218bea
--- /dev/null
+++ b/service/java/com/android/server/uwb/data/UwbUciConstants.java
@@ -0,0 +1,186 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.data;
+
+import static android.hardware.uwb.fira_android.UwbVendorSessionInitSessionType.CCC;
+import static android.hardware.uwb.fira_android.UwbVendorStatusCodes.STATUS_ERROR_CCC_LIFECYCLE;
+import static android.hardware.uwb.fira_android.UwbVendorStatusCodes.STATUS_ERROR_CCC_SE_BUSY;
+
+import com.google.uwb.support.fira.FiraParams;
+
+public class UwbUciConstants {
+    /**
+     * Table 10:Device State Values
+     */
+    public static final byte DEVICE_STATE_OFF = 0x00; //NOT defined in the UCI spec
+    public static final byte DEVICE_STATE_READY = 0x01;
+    public static final byte DEVICE_STATE_ACTIVE = 0x02;
+    public static final byte DEVICE_STATE_ERROR = (byte) 0xFF;
+
+    public static final byte UWBS_RESET = 0x00;
+
+    /**
+     * Table 13: Control Messages to Initialize UWB session
+     */
+    public static final byte SESSION_TYPE_RANGING = 0x00;
+    public static final byte SESSION_TYPE_DATA_TRANSFER = 0x01;
+    public static final byte SESSION_TYPE_CCC = (byte) CCC;
+    public static final byte SESSION_TYPE_DEVICE_TEST_MODE = (byte) 0xD0;
+
+    /**
+     * Table 14: Control Messages to De-Initialize UWB session - SESSION_STATUS_NTF
+     * RangingSession.State
+     */
+    public static final int UWB_SESSION_STATE_INIT = 0x00;
+    public static final int UWB_SESSION_STATE_DEINIT = 0x01;
+    public static final int UWB_SESSION_STATE_ACTIVE = 0x02;
+    public static final int UWB_SESSION_STATE_IDLE = 0x03;
+    public static final int UWB_SESSION_STATE_ERROR = 0xFF;
+
+    /**
+     * Table 15: state change with reason codes
+     */
+    public static final int REASON_STATE_CHANGE_WITH_SESSION_MANAGEMENT_COMMANDS = 0x00;
+    /* Below reason codes shall be reported with SESSION_STATE_IDLE state only. */
+    public static final int REASON_MAX_RANGING_ROUND_RETRY_COUNT_REACHED = 0x01;
+    public static final int REASON_MAX_NUMBER_OF_MEASUREMENTS_REACHED = 0x02;
+    public static final int REASON_ERROR_SLOT_LENGTH_NOT_SUPPORTED = 0x20;
+    public static final int REASON_ERROR_INSUFFICIENT_SLOTS_PER_RR = 0x21;
+    public static final int REASON_ERROR_MAC_ADDRESS_MODE_NOT_SUPPORTED = 0x22;
+    public static final int REASON_ERROR_INVALID_RANGING_INTERVAL = 0x23;
+    public static final int REASON_ERROR_INVALID_STS_CONFIG = 0x24;
+    public static final int REASON_ERROR_INVALID_RFRAME_CONFIG = 0x25;
+
+    /**
+     * Table 27: Multicast list update status codes
+     */
+    /* Multicast update status codes */
+    public static final int MULTICAST_LIST_UPDATE_STATUS_OK = 0x00;
+    public static final int MULTICAST_LIST_UPDATE_STATUS_ERROR_FULL = 0x01;
+    public static final int MULTICAST_LIST_UPDATE_STATUS_ERROR_KEY_FETCH_FAIL = 0x02;
+    public static final int MULTICAST_LIST_UPDATE_STATUS_ERROR_SUB_SESSION_ID_NOT_FOUND = 0x03;
+
+    /**
+     * Table 29:APP Configuration Parameters IDs
+     */
+    public static final int DEVICE_TYPE_CONTROLEE = FiraParams.RANGING_DEVICE_TYPE_CONTROLEE;
+    public static final int DEVICE_TYPE_CONTROLLER = FiraParams.RANGING_DEVICE_TYPE_CONTROLLER;
+
+    public static final int ROUND_USAGE_SS_TWR_DEFERRED_MODE =
+            FiraParams.RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE;
+    public static final int ROUND_USAGE_DS_TWR_DEFERRED_MODE =
+            FiraParams.RANGING_ROUND_USAGE_DS_TWR_DEFERRED_MODE;
+    public static final int ROUND_USAGE_SS_TWR_NON_DEFERRED_MODE =
+            FiraParams.RANGING_ROUND_USAGE_SS_TWR_NON_DEFERRED_MODE;
+    public static final int ROUND_USAGE_DS_TWR_NON_DEFERRED_MODE =
+            FiraParams.RANGING_ROUND_USAGE_DS_TWR_NON_DEFERRED_MODE;
+
+    public static final int MULTI_NODE_MODE_UNICAST = FiraParams.MULTI_NODE_MODE_UNICAST;
+    public static final int MULTI_NODE_MODE_ONE_TO_MANY = FiraParams.MULTI_NODE_MODE_ONE_TO_MANY;
+    public static final int MULTI_NODE_MODE_MANY_TO_MANY = FiraParams.MULTI_NODE_MODE_MANY_TO_MANY;
+
+    public static final int CHANNEL_5 = FiraParams.UWB_CHANNEL_5;
+    public static final int CHANNEL_6 = FiraParams.UWB_CHANNEL_6;
+    public static final int CHANNEL_8 = FiraParams.UWB_CHANNEL_8;
+    public static final int CHANNEL_9 = FiraParams.UWB_CHANNEL_9;
+    public static final int CHANNEL_10 = FiraParams.UWB_CHANNEL_10;
+    public static final int CHANNEL_12 = FiraParams.UWB_CHANNEL_12;
+    public static final int CHANNEL_13 = FiraParams.UWB_CHANNEL_13;
+    public static final int CHANNEL_14 = FiraParams.UWB_CHANNEL_14;
+
+    public static final int MAC_FCS_TYPE_CRC_16 = FiraParams.MAC_FCS_TYPE_CRC_16;
+    public static final int MAC_FCS_TYPE_CRC_32 = FiraParams.MAC_FCS_TYPE_CRC_32;
+
+    public static final int AOA_RESULT_REQ_DISABLE = 0x00;
+    public static final int AOA_RESULT_REQ_ENABLE = 0x01;
+
+    public static final int RANGE_DATA_NTF_CONFIG_DISABLE =
+            FiraParams.RANGE_DATA_NTF_CONFIG_DISABLE;
+    public static final int RANGE_DATA_NTF_CONFIG_ENABLE = FiraParams.RANGE_DATA_NTF_CONFIG_ENABLE;
+    public static final int RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY =
+            FiraParams.RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY;
+
+    public static final int RANGING_DEVICE_ROLE_RESPONDER =
+            FiraParams.RANGING_DEVICE_ROLE_RESPONDER;
+    public static final int RANGING_DEVICE_ROLE_INITIATOR =
+            FiraParams.RANGING_DEVICE_ROLE_INITIATOR;
+
+    public static final byte RANGING_MEASUREMENT_TYPE_TWO_WAY = 0X01;
+
+    /**
+     * Table 32: Status Codes
+     */
+    /* Generic Status Codes */
+    public static final int STATUS_CODE_OK = FiraParams.STATUS_CODE_OK;
+    public static final int STATUS_CODE_REJECTED = FiraParams.STATUS_CODE_REJECTED;
+    public static final int STATUS_CODE_FAILED = FiraParams.STATUS_CODE_FAILED;
+    public static final int STATUS_CODE_SYNTAX_ERROR = FiraParams.STATUS_CODE_SYNTAX_ERROR;
+    public static final int STATUS_CODE_INVALID_PARAM = FiraParams.STATUS_CODE_INVALID_PARAM;
+    public static final int STATUS_CODE_INVALID_RANGE = FiraParams.STATUS_CODE_INVALID_RANGE;
+    public static final int STATUS_CODE_INVALID_MESSAGE_SIZE =
+            FiraParams.STATUS_CODE_INVALID_MESSAGE_SIZE;
+    public static final int STATUS_CODE_UNKNOWN_GID = FiraParams.STATUS_CODE_UNKNOWN_GID;
+    public static final int STATUS_CODE_UNKNOWN_OID = FiraParams.STATUS_CODE_UNKNOWN_OID;
+    public static final int STATUS_CODE_READ_ONLY = FiraParams.STATUS_CODE_READ_ONLY;
+    public static final int STATUS_CODE_COMMAND_RETRY = FiraParams.STATUS_CODE_COMMAND_RETRY;
+    /* UWB Session Specific Status Codes */
+    public static final int STATUS_CODE_ERROR_SESSION_NOT_EXIST =
+            FiraParams.STATUS_CODE_ERROR_SESSION_NOT_EXIST;
+    public static final int STATUS_CODE_ERROR_SESSION_DUPLICATE =
+            FiraParams.STATUS_CODE_ERROR_SESSION_DUPLICATE;
+    public static final int STATUS_CODE_ERROR_SESSION_ACTIVE =
+            FiraParams.STATUS_CODE_ERROR_SESSION_ACTIVE;
+    public static final int STATUS_CODE_ERROR_MAX_SESSIONS_EXCEEDED =
+            FiraParams.STATUS_CODE_ERROR_MAX_SESSIONS_EXCEEDED;
+    public static final int STATUS_CODE_ERROR_SESSION_NOT_CONFIGURED =
+            FiraParams.STATUS_CODE_ERROR_SESSION_NOT_CONFIGURED;
+    public static final int STATUS_CODE_ERROR_ACTIVE_SESSIONS_ONGOING =
+            FiraParams.STATUS_CODE_ERROR_ACTIVE_SESSIONS_ONGOING;
+    public static final int STATUS_CODE_ERROR_MULTICAST_LIST_FULL =
+            FiraParams.STATUS_CODE_ERROR_MULTICAST_LIST_FULL;
+    public static final int STATUS_CODE_ERROR_ADDRESS_NOT_FOUND =
+            FiraParams.STATUS_CODE_ERROR_ADDRESS_NOT_FOUND;
+    public static final int STATUS_CODE_ERROR_ADDRESS_ALREADY_PRESENT =
+            FiraParams.STATUS_CODE_ERROR_ADDRESS_ALREADY_PRESENT;
+    /* UWB Ranging Session Specific Status Codes */
+    public static final int STATUS_CODE_RANGING_TX_FAILED =
+            FiraParams.STATUS_CODE_RANGING_TX_FAILED;
+    public static final int STATUS_CODE_RANGING_RX_TIMEOUT =
+            FiraParams.STATUS_CODE_RANGING_RX_TIMEOUT;
+    public static final int STATUS_CODE_RANGING_RX_PHY_DEC_FAILED =
+            FiraParams.STATUS_CODE_RANGING_RX_PHY_DEC_FAILED;
+    public static final int STATUS_CODE_RANGING_RX_PHY_TOA_FAILED =
+            FiraParams.STATUS_CODE_RANGING_RX_PHY_TOA_FAILED;
+    public static final int STATUS_CODE_RANGING_RX_PHY_STS_FAILED =
+            FiraParams.STATUS_CODE_RANGING_RX_PHY_STS_FAILED;
+    public static final int STATUS_CODE_RANGING_RX_MAC_DEC_FAILED =
+            FiraParams.STATUS_CODE_RANGING_RX_MAC_DEC_FAILED;
+    public static final int STATUS_CODE_RANGING_RX_MAC_IE_DEC_FAILED =
+            FiraParams.STATUS_CODE_RANGING_RX_MAC_IE_DEC_FAILED;
+    public static final int STATUS_CODE_RANGING_RX_MAC_IE_MISSING =
+            FiraParams.STATUS_CODE_RANGING_RX_MAC_IE_MISSING;
+
+    public static final int STATUS_CODE_CCC_SE_BUSY = STATUS_ERROR_CCC_SE_BUSY;
+    public static final int STATUS_CODE_CCC_LIFECYCLE = STATUS_ERROR_CCC_LIFECYCLE;
+
+    /* UWB Data Session Specific Status Codes */
+    public static final int STATUS_CODE_DATA_MAX_TX_APDU_SIZE_EXCEEDED = 0x30;
+    public static final int STATUS_CODE_DATA_RX_CRC_ERROR = 0x31;
+
+    /* UWB STS Mode Codes */
+    public static final int STS_MODE_STATIC = 0x00;
+    public static final int STS_MODE_DYNAMIC = 0x01;
+}
\ No newline at end of file
diff --git a/service/java/com/android/server/uwb/data/UwbVendorUciResponse.java b/service/java/com/android/server/uwb/data/UwbVendorUciResponse.java
new file mode 100644
index 0000000..590f492
--- /dev/null
+++ b/service/java/com/android/server/uwb/data/UwbVendorUciResponse.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.data;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+public class UwbVendorUciResponse {
+    public byte status;
+    public byte[] payload;
+    public int gid;
+    public int oid;
+
+    public UwbVendorUciResponse(byte status, int gid, int oid, byte[] payload) {
+        this.status = status;
+        this.gid = gid;
+        this.oid = oid;
+        this.payload = payload;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof UwbVendorUciResponse)) return false;
+        UwbVendorUciResponse that = (UwbVendorUciResponse) o;
+        return status == that.status && gid == that.gid && oid == that.oid
+                && Arrays.equals(payload, that.payload);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(status, gid, oid, Arrays.hashCode(payload));
+    }
+
+    @Override
+    public String toString() {
+        return "UwbVendorUciResponse{"
+                + "status=" + status
+                + ", gid=" + gid
+                + ", oid=" + oid
+                + ", payload=" + Arrays.toString(payload)
+                + '}';
+    }
+}
diff --git a/service/java/com/android/server/uwb/discovery/ble/DiscoveryAdvertisement.java b/service/java/com/android/server/uwb/discovery/ble/DiscoveryAdvertisement.java
new file mode 100644
index 0000000..59801d7
--- /dev/null
+++ b/service/java/com/android/server/uwb/discovery/ble/DiscoveryAdvertisement.java
@@ -0,0 +1,379 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.discovery.ble;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import com.android.server.uwb.discovery.info.FiraProfileSupportInfo;
+import com.android.server.uwb.discovery.info.RegulatoryInfo;
+import com.android.server.uwb.discovery.info.UwbIndicationData;
+import com.android.server.uwb.discovery.info.VendorSpecificData;
+import com.android.server.uwb.util.ArrayUtils;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+import com.android.server.uwb.util.Hex;
+
+import com.google.common.primitives.Bytes;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Holds data of the BLE discovery advertisement according to FiRa BLE OOB v1.0 specification.
+ */
+public class DiscoveryAdvertisement {
+    private static final String LOG_TAG = DiscoveryAdvertisement.class.getSimpleName();
+
+    // The FiRa service UUID for connector primary and connector secondary as defined in Bluetooth
+    // Specification Supplement v10.
+    public static final String FIRA_CP_SERVICE_UUID = "FFF3";
+    public static final String FIRA_CS_SERVICE_UUID = "FFF4";
+
+    /**
+     * Generate a Parcelable wrapper around UUID.
+     *
+     * @param uuid 16-bit ID (4 characters hex) assigned by Bluetooth specification for a particular
+     *     service.
+     * @return full 128-bit {@link ParcelUuid}, else null if invalid.
+     */
+    public static ParcelUuid getParcelUuid(String uuid) {
+        if (uuid.length() != 4) {
+            throw new IllegalStateException(
+                    String.format(
+                            "Failed to getParcelUuid from UUID string %s. UUID is expected to be 4"
+                                    + " characters",
+                            uuid));
+        }
+        return ParcelUuid.fromString("0000" + uuid + "-0000-1000-8000-00805F9B34FB");
+    }
+
+    // Size of the fields inside the advertisement.
+    private static final int LENGTH_SIZE = 1;
+    private static final int DATA_TYPE_SIZE = 1;
+    private static final int SERVICE_UUID_SIZE = 2;
+
+    private static final int MIN_ADVETISEMENT_SIZE =
+            LENGTH_SIZE + DATA_TYPE_SIZE + SERVICE_UUID_SIZE;
+
+    // Data type field value assigned by the Bluetooth GAP.
+    private static final byte DATA_TYPE = 0x16;
+
+    // Mask and value of the FiRa specific field type field within each AD field.
+    private static final byte FIRA_SPECIFIC_FIELD_TYPE_MASK = (byte) 0xF0;
+    private static final byte FIRA_SPECIFIC_FIELD_TYPE_UWB_INDICATION_DATA = 0x1;
+    private static final byte FIRA_SPECIFIC_FIELD_TYPE_VENDOR_SPECIFIC_DATA = 0x2;
+    private static final byte FIRA_SPECIFIC_FIELD_TYPE_UWB_REGULATORY_INFO = 0x3;
+    private static final byte FIRA_SPECIFIC_FIELD_TYPE_FIRA_PROFILE_SUPPORT_INFO = 0x4;
+
+    // FiRa specific field length field within each AD field.
+    private static final byte FIRA_SPECIFIC_FIELD_LENGTH_MASK = 0x0F;
+
+    public final String serviceUuid;
+    public final UwbIndicationData uwbIndicationData;
+    public final RegulatoryInfo regulatoryInfo;
+    public final FiraProfileSupportInfo firaProfileSupportInfo;
+    public final VendorSpecificData[] vendorSpecificData;
+
+    /**
+     * Generate the DiscoveryAdvertisement from raw bytes arrays.
+     *
+     * @param serviceData byte array containing the UWB BLE Advertiser Service Data encoding based
+     *     on the FiRa specification.
+     * @param manufacturerSpecificData byte array containing the UWB BLE Advertiser Manufacturer
+     *     Specific Data encoding based on the FiRa specification.
+     * @return decode bytes into {@link DiscoveryAdvertisement}, else null if invalid.
+     */
+    @Nullable
+    public static DiscoveryAdvertisement fromBytes(
+            @Nullable byte[] serviceData, @Nullable byte[] manufacturerSpecificData) {
+        if (ArrayUtils.isEmpty(serviceData)) {
+            logw("Failed to convert empty into BLE Discovery advertisement.");
+            return null;
+        }
+
+        if (serviceData.length < MIN_ADVETISEMENT_SIZE) {
+            logw(
+                    "Failed to convert bytes into BLE Discovery advertisement due to invalid"
+                            + " advertisement size.");
+            return null;
+        }
+
+        ByteBuffer byteBuffer = ByteBuffer.wrap(serviceData);
+        int length = Byte.toUnsignedInt(byteBuffer.get());
+        if (length != serviceData.length - LENGTH_SIZE) {
+            logw(
+                    "Failed to convert bytes into BLE Discovery advertisement due to unmatched"
+                            + " advertisement size.");
+            return null;
+        }
+
+        byte dataType = byteBuffer.get();
+        if (dataType != DATA_TYPE) {
+            logw(
+                    "Failed to convert bytes into BLE Discovery advertisement due to unmatched"
+                            + " advertisement data type.");
+            return null;
+        }
+        // In little endian encoding
+        byte[] serviceUuidBytes = new byte[SERVICE_UUID_SIZE];
+        byteBuffer.get(serviceUuidBytes);
+        String serviceUuid = Hex.encodeUpper(new byte[] {serviceUuidBytes[1], serviceUuidBytes[0]});
+        if (!serviceUuid.equals(FIRA_CP_SERVICE_UUID)
+                && !serviceUuid.equals(FIRA_CS_SERVICE_UUID)) {
+            logw(
+                    "Failed to convert bytes into BLE Discovery advertisement due to invalid FiRa"
+                            + " advertisement service uuid="
+                            + serviceUuid);
+            return null;
+        }
+
+        UwbIndicationData uwbIndicationData = null;
+        RegulatoryInfo regulatoryInfo = null;
+        FiraProfileSupportInfo firaProfileSupportInfo = null;
+        List<VendorSpecificData> vendorSpecificData = new ArrayList<>();
+
+        while (byteBuffer.hasRemaining()) {
+            // Parsing the next block of FiRa specific field based on given field type and length.
+            byte firstByte = byteBuffer.get();
+            byte fieldType = (byte) ((firstByte & FIRA_SPECIFIC_FIELD_TYPE_MASK) >> 4);
+            byte fieldLength = (byte) (firstByte & FIRA_SPECIFIC_FIELD_LENGTH_MASK);
+            if (byteBuffer.remaining() < fieldLength) {
+                logw(
+                        "Failed to convert bytes into BLE Discovery advertisement due to byte"
+                                + " ended unexpectedly.");
+                return null;
+            }
+            byte[] fieldBytes = new byte[fieldLength];
+            byteBuffer.get(fieldBytes);
+
+            if (fieldType == FIRA_SPECIFIC_FIELD_TYPE_UWB_INDICATION_DATA) {
+                if (uwbIndicationData != null) {
+                    logw(
+                            "Failed to convert bytes into BLE Discovery advertisement due to"
+                                    + " duplicate uwb indication data field.");
+                    return null;
+                }
+                uwbIndicationData = UwbIndicationData.fromBytes(fieldBytes);
+            } else if (fieldType == FIRA_SPECIFIC_FIELD_TYPE_UWB_REGULATORY_INFO) {
+                if (regulatoryInfo != null) {
+                    logw(
+                            "Failed to convert bytes into BLE Discovery advertisement due to"
+                                    + " duplicate regulatory info field.");
+                    return null;
+                }
+                regulatoryInfo = RegulatoryInfo.fromBytes(fieldBytes);
+            } else if (fieldType == FIRA_SPECIFIC_FIELD_TYPE_FIRA_PROFILE_SUPPORT_INFO) {
+                if (firaProfileSupportInfo != null) {
+                    logw(
+                            "Failed to convert bytes into BLE Discovery advertisement due to"
+                                    + " duplicate FiRa profile support info field.");
+                    return null;
+                }
+                firaProfileSupportInfo = FiraProfileSupportInfo.fromBytes(fieldBytes);
+            } else if (fieldType == FIRA_SPECIFIC_FIELD_TYPE_VENDOR_SPECIFIC_DATA) {
+                // There can be multiple Vendor specific data fields.
+                VendorSpecificData data = VendorSpecificData.fromBytes(fieldBytes);
+                if (data != null) {
+                    vendorSpecificData.add(data);
+                }
+            } else {
+                logw(
+                        "Failed to convert bytes into BLE Discovery advertisement due to invalid"
+                                + " field type "
+                                + fieldType);
+                return null;
+            }
+        }
+
+        // product/implementation specific data inside “Service Data” AD type object with CS UUID.
+        // It should be used only if the GAP Advertiser role doesn’t support exposing “Manufacturer
+        // Specific Data” AD type object.
+        if (!ArrayUtils.isEmpty(manufacturerSpecificData)) {
+            ByteBuffer vendorByteBuffer = ByteBuffer.wrap(manufacturerSpecificData);
+            byte firstByte = vendorByteBuffer.get();
+            byte fieldType = (byte) ((firstByte & FIRA_SPECIFIC_FIELD_TYPE_MASK) >> 4);
+            byte fieldLength = (byte) (firstByte & FIRA_SPECIFIC_FIELD_LENGTH_MASK);
+            if (fieldType == FIRA_SPECIFIC_FIELD_TYPE_VENDOR_SPECIFIC_DATA) {
+                if (vendorByteBuffer.remaining() < fieldLength) {
+                    logw(
+                            "Failed to convert bytes into BLE Discovery advertisement due to"
+                                    + " manufacturer specific data ended unexpectedly.");
+                    return null;
+                }
+                byte[] fieldBytes = new byte[fieldLength];
+                vendorByteBuffer.get(fieldBytes);
+                VendorSpecificData data = VendorSpecificData.fromBytes(fieldBytes);
+                if (!vendorSpecificData.isEmpty()) {
+                    logw(
+                            "Failed to convert bytes into BLE Discovery advertisement due to Vendor"
+                                + " Specific Data exist in both Service Data AD and Manufacturer"
+                                + " Specific Data AD.");
+                    return null;
+                }
+                vendorSpecificData.add(data);
+            }
+        }
+
+        return new DiscoveryAdvertisement(
+                serviceUuid,
+                uwbIndicationData,
+                regulatoryInfo,
+                firaProfileSupportInfo,
+                vendorSpecificData.toArray(new VendorSpecificData[0]));
+    }
+
+    /**
+     * Generate raw bytes array from DiscoveryAdvertisement.
+     *
+     * @param adv the UWB BLE discovery Advertisement.
+     * @param includeVendorSpecificData specify if the vendorSpecificData to be included in the
+     *     advertisement bytes.
+     * @return encoded bytes into byte array based on the FiRa specification.
+     */
+    public static byte[] toBytes(
+            @NonNull DiscoveryAdvertisement adv, boolean includeVendorSpecificData) {
+        byte[] data = convertMetadata(adv.serviceUuid);
+
+        if (adv.uwbIndicationData != null) {
+            data = Bytes.concat(data, convertUwbIndicationData(adv.uwbIndicationData));
+        }
+        if (adv.regulatoryInfo != null) {
+            data = Bytes.concat(data, convertRegulatoryInfo(adv.regulatoryInfo));
+        }
+        if (adv.firaProfileSupportInfo != null) {
+            data = Bytes.concat(data, convertFiraProfileSupportInfo(adv.firaProfileSupportInfo));
+        }
+        if (includeVendorSpecificData) {
+            for (VendorSpecificData d : adv.vendorSpecificData) {
+                data = Bytes.concat(data, convertVendorSpecificData(d));
+            }
+        }
+
+        return Bytes.concat(new byte[] {convertByteLength(data.length)}, data);
+    }
+
+    /**
+     * Generate raw bytes array from DiscoveryAdvertisement.vendorSpecificData.
+     *
+     * @param adv the UWB BLE discovery Advertisement.
+     * @return encoded Manufacturer Specific Data into byte array based on the FiRa specification.
+     */
+    public static byte[] getManufacturerSpecificDataInBytes(@NonNull DiscoveryAdvertisement adv) {
+        if (adv.vendorSpecificData.length > 0) {
+            return convertVendorSpecificData(adv.vendorSpecificData[0]);
+        }
+        return null;
+    }
+
+    private static byte[] convertMetadata(String serviceUuid) {
+        byte[] uuidBytes = Hex.decode(serviceUuid);
+        return new byte[] {DATA_TYPE, uuidBytes[1], uuidBytes[0]};
+    }
+
+    private static byte convertByteLength(int size) {
+        return DataTypeConversionUtil.i32ToByteArray(size)[3];
+    }
+
+    private static byte[] convertUwbIndicationData(UwbIndicationData uwbIndicationData) {
+        byte[] data = UwbIndicationData.toBytes(uwbIndicationData);
+        return Bytes.concat(
+                new byte[] {
+                    (byte)
+                            (((FIRA_SPECIFIC_FIELD_TYPE_UWB_INDICATION_DATA << 4)
+                                            & FIRA_SPECIFIC_FIELD_TYPE_MASK)
+                                    | (convertByteLength(data.length)
+                                            & FIRA_SPECIFIC_FIELD_LENGTH_MASK))
+                },
+                data);
+    }
+
+    private static byte[] convertRegulatoryInfo(RegulatoryInfo regulatoryInfo) {
+        byte[] data = RegulatoryInfo.toBytes(regulatoryInfo);
+        return Bytes.concat(
+                new byte[] {
+                    (byte)
+                            (((FIRA_SPECIFIC_FIELD_TYPE_UWB_REGULATORY_INFO << 4)
+                                            & FIRA_SPECIFIC_FIELD_TYPE_MASK)
+                                    | (convertByteLength(data.length)
+                                            & FIRA_SPECIFIC_FIELD_LENGTH_MASK))
+                },
+                data);
+    }
+
+    private static byte[] convertFiraProfileSupportInfo(
+            FiraProfileSupportInfo firaProfileSupportInfo) {
+        byte[] data = FiraProfileSupportInfo.toBytes(firaProfileSupportInfo);
+        return Bytes.concat(
+                new byte[] {
+                    (byte)
+                            (((FIRA_SPECIFIC_FIELD_TYPE_FIRA_PROFILE_SUPPORT_INFO << 4)
+                                            & FIRA_SPECIFIC_FIELD_TYPE_MASK)
+                                    | (convertByteLength(data.length)
+                                            & FIRA_SPECIFIC_FIELD_LENGTH_MASK))
+                },
+                data);
+    }
+
+    private static byte[] convertVendorSpecificData(VendorSpecificData vendorSpecificData) {
+        byte[] data = VendorSpecificData.toBytes(vendorSpecificData);
+        return Bytes.concat(
+                new byte[] {
+                    (byte)
+                            (((FIRA_SPECIFIC_FIELD_TYPE_VENDOR_SPECIFIC_DATA << 4)
+                                            & FIRA_SPECIFIC_FIELD_TYPE_MASK)
+                                    | (convertByteLength(data.length)
+                                            & FIRA_SPECIFIC_FIELD_LENGTH_MASK))
+                },
+                data);
+    }
+
+    public DiscoveryAdvertisement(
+            String serviceUuid,
+            @Nullable UwbIndicationData uwbIndicationData,
+            @Nullable RegulatoryInfo regulatoryInfo,
+            @Nullable FiraProfileSupportInfo firaProfileSupportInfo,
+            @Nullable VendorSpecificData[] vendorSpecificData) {
+        this.serviceUuid = serviceUuid;
+        this.uwbIndicationData = uwbIndicationData;
+        this.regulatoryInfo = regulatoryInfo;
+        this.firaProfileSupportInfo = firaProfileSupportInfo;
+        this.vendorSpecificData = vendorSpecificData;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("DiscoveryAdvertisement: serviceUuid=")
+                .append(serviceUuid)
+                .append(" uwbIndicationData={")
+                .append(uwbIndicationData)
+                .append("} regulatoryInfo={")
+                .append(regulatoryInfo)
+                .append("} firaProfileSupportInfo={")
+                .append(firaProfileSupportInfo)
+                .append("} ")
+                .append(Arrays.toString(vendorSpecificData));
+        return sb.toString();
+    }
+
+    private static void logw(String log) {
+        Log.w(LOG_TAG, log);
+    }
+}
diff --git a/service/java/com/android/server/uwb/discovery/info/ChannelPowerInfo.java b/service/java/com/android/server/uwb/discovery/info/ChannelPowerInfo.java
new file mode 100644
index 0000000..642d271
--- /dev/null
+++ b/service/java/com/android/server/uwb/discovery/info/ChannelPowerInfo.java
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.discovery.info;
+
+import android.annotation.NonNull;
+import android.util.Log;
+
+import com.android.server.uwb.util.ArrayUtils;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+/**
+ * UWB channel power information according to FiRa BLE OOB v1.0
+ * specification.
+ */
+public class ChannelPowerInfo {
+    private static final String LOG_TAG = ChannelPowerInfo.class.getSimpleName();
+
+    // Minimum size of the full info
+    private static final int MIN_CHANNEL_POWER_INFO_SIZE = 2;
+
+    private static final int ENCODE_1ST_CHANNEL_BITMASK = 0xF0;
+    private static final byte ENCODE_NUM_OF_CHANNEL_BITMASK = 0x0E;
+    private static final byte ENCODE_OUTDOOR_OR_INDOOR_BITMASK = 0x01;
+
+    public final int firstChannel;
+    public final int numOfChannels;
+    public final boolean isIndoor;
+    public final int averagePowerLimitDbm;
+
+    /**
+     * Generate the ChannelPowerInfo from raw bytes array.
+     *
+     * @param bytes byte array containing the channel power info as part of the UWB regulatory,
+     *     data encoding based on the FiRa specification.
+     * @return decode bytes into {@link ChannelPowerInfo}.
+     */
+    public static ChannelPowerInfo fromBytes(@NonNull byte[] bytes) {
+        if (ArrayUtils.isEmpty(bytes)) {
+            Log.w(LOG_TAG, "Failed to convert empty into UWB channel power info.");
+            return null;
+        }
+
+        if (bytes.length < MIN_CHANNEL_POWER_INFO_SIZE) {
+            Log.w(
+                    LOG_TAG,
+                    "Failed to convert bytes into UWB channel power info due to invalid data"
+                            + " size.");
+            return null;
+        }
+
+        int firstChannel = (int) (((bytes[0] & ENCODE_1ST_CHANNEL_BITMASK) >> 4) & 0x000F);
+        int numOfChannels = (int) (((bytes[0] & ENCODE_NUM_OF_CHANNEL_BITMASK) >> 1) & 0x0007);
+        boolean isIndoor = (bytes[0] & ENCODE_OUTDOOR_OR_INDOOR_BITMASK) != 0;
+        int averagePowerLimitDbm = (int) bytes[1];
+        return new ChannelPowerInfo(firstChannel, numOfChannels, isIndoor, averagePowerLimitDbm);
+    }
+
+    /**
+     * Generate raw bytes array from ChannelPowerInfo.
+     *
+     * @param info the channel power data.
+     * @return encoded bytes into byte array based on the FiRa specification.
+     */
+    public static byte[] toBytes(@NonNull ChannelPowerInfo info) {
+        return new byte[] {
+            (byte)
+                    (convertFirstChannel(info.firstChannel)
+                            | convertNumOfChannels(info.numOfChannels)
+                            | convertIsIndoor(info.isIndoor)),
+            DataTypeConversionUtil.i32ToByteArray(info.averagePowerLimitDbm)[3]
+        };
+    }
+
+    private static byte convertFirstChannel(int firstChannel) {
+        return (byte) ((firstChannel << 4) & ENCODE_1ST_CHANNEL_BITMASK);
+    }
+
+    private static byte convertNumOfChannels(int numOfChannels) {
+        return (byte) ((numOfChannels << 1) & ENCODE_NUM_OF_CHANNEL_BITMASK);
+    }
+
+    private static byte convertIsIndoor(boolean isIndoor) {
+        return (byte) ((isIndoor ? 1 : 0) & ENCODE_OUTDOOR_OR_INDOOR_BITMASK);
+    }
+
+    public ChannelPowerInfo(
+            int firstChannel, int numOfChannels, boolean isIndoor, int averagePowerLimitDbm) {
+        this.firstChannel = firstChannel;
+        this.isIndoor = isIndoor;
+        this.numOfChannels = numOfChannels;
+        this.averagePowerLimitDbm = averagePowerLimitDbm;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("ChannelPowerInfo: mFirstChannel=")
+                .append(firstChannel)
+                .append(" mNumOfChannels=")
+                .append(numOfChannels)
+                .append(" mIsIndoor=")
+                .append(isIndoor)
+                .append(" mAveragePowerLimitDbm=")
+                .append(averagePowerLimitDbm);
+        return sb.toString();
+    }
+}
diff --git a/service/java/com/android/server/uwb/discovery/info/FiraProfileSupportInfo.java b/service/java/com/android/server/uwb/discovery/info/FiraProfileSupportInfo.java
new file mode 100644
index 0000000..80f85ef
--- /dev/null
+++ b/service/java/com/android/server/uwb/discovery/info/FiraProfileSupportInfo.java
@@ -0,0 +1,161 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.discovery.info;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import com.android.server.uwb.util.ArrayUtils;
+
+import com.google.common.primitives.Bytes;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Holds data of the FiRa UWB Profile support info according to FiRa BLE OOB v1.0 and CSML v1.0
+ * specification.
+ */
+public class FiraProfileSupportInfo {
+
+    private static final String LOG_TAG = FiraProfileSupportInfo.class.getSimpleName();
+
+    /**
+     * FiRa defined profiles with ID.
+     */
+    public enum FiraProfile {
+        PACS(1); // Physical Access Control System
+
+        private final int mId;
+        private static Map sMap = new HashMap<>();
+
+        FiraProfile(int id) {
+            this.mId = id;
+        }
+
+        static {
+            for (FiraProfile profile : FiraProfile.values()) {
+                sMap.put(profile.mId, profile);
+            }
+        }
+
+        /**
+         * Get the FiraProfile based on the given ID.
+         *
+         * @param id profile ID defined by FiRa.
+         * @return {@link FiraProfile} associated with the id, else null if invalid.
+         */
+        @Nullable
+        public static FiraProfile idOf(int id) {
+            return (FiraProfile) sMap.get(id);
+        }
+
+        public int getId() {
+            return mId;
+        }
+    }
+
+    public final FiraProfile[] supportedFiraProfiles;
+
+    /**
+     * Generate the FiraProfileSupportInfo from raw bytes array.
+     *
+     * @param bytes byte array containing the FiRa UWB Profile support data encoding based on the
+     *     FiRa specification. Nth bit represents FiRa Service ID “N+1”. Bit 0 (the least
+     *     significant) represents FiRa Service ID 1.
+     * @return decode bytes into {@link FiraProfileSupportInfo}, else null if invalid.
+     */
+    @Nullable
+    public static FiraProfileSupportInfo fromBytes(@NonNull byte[] bytes) {
+        if (ArrayUtils.isEmpty(bytes)) {
+            logw("Failed to convert empty into FiRa Profile Support Info.");
+            return null;
+        }
+
+        List<FiraProfile> supportedProfiles = new ArrayList<>();
+
+        int current_id = 1;
+        // Loop through each byte start from the least significant byte.
+        for (int i = 1; i <= bytes.length; i++) {
+            // Loop through each bit start from the least significant bit.
+            byte b = bytes[bytes.length - i];
+            for (int j = 0; j < Byte.SIZE; j++) {
+                if ((b & (0x1 << j)) != 0) {
+                    FiraProfile profile = FiraProfile.idOf(current_id);
+                    if (profile != null) {
+                        supportedProfiles.add(profile);
+                    } else {
+                        logw("Invalid Profile ID in FiRa Profile Support Info. ID=" + current_id);
+                    }
+                }
+                current_id++;
+            }
+        }
+
+        return new FiraProfileSupportInfo(supportedProfiles.toArray(new FiraProfile[0]));
+    }
+
+    /**
+     * Generate raw bytes array from FiraProfileSupportInfo.
+     *
+     * @param info the UWB regulatory data.
+     * @return encoded bytes into byte array based on the FiRa specification.
+     */
+    public static byte[] toBytes(@NonNull FiraProfileSupportInfo info) {
+        List<Byte> byteList = new ArrayList<>(); // Little-endian
+
+        for (FiraProfile profile : info.supportedFiraProfiles) {
+            int bit_position = profile.getId() - 1;
+            int nth_byte = bit_position / Byte.SIZE;
+            byte nth_bit = (byte) (bit_position % Byte.SIZE);
+            // Extends the byteList will zeros
+            if (byteList.size() <= nth_byte) {
+                byteList.addAll(Bytes.asList(new byte[1 + nth_byte - byteList.size()]));
+            }
+            byte b = (byte) (byteList.get(nth_byte).byteValue() | (0x1 << nth_bit));
+            byteList.set(nth_byte, Byte.valueOf(b));
+        }
+
+        byte[] data = new byte[byteList.size()];
+        // Convert to big-endian byte array
+        for (int i = 0; i < byteList.size(); i++) {
+            data[i] = byteList.get(byteList.size() - i - 1).byteValue();
+        }
+
+        return data;
+    }
+
+    public FiraProfileSupportInfo(FiraProfile[] supportedFiraProfiles) {
+        this.supportedFiraProfiles = supportedFiraProfiles;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("FiraProfileSupportInfo: SupportedFiraProfiles=")
+                .append(Arrays.toString(supportedFiraProfiles));
+        return sb.toString();
+    }
+
+    private static void logw(String log) {
+        Log.w(LOG_TAG, log);
+    }
+}
diff --git a/service/java/com/android/server/uwb/discovery/info/RegulatoryInfo.java b/service/java/com/android/server/uwb/discovery/info/RegulatoryInfo.java
new file mode 100644
index 0000000..292c649
--- /dev/null
+++ b/service/java/com/android/server/uwb/discovery/info/RegulatoryInfo.java
@@ -0,0 +1,259 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.discovery.info;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import com.android.server.uwb.UwbCountryCode;
+import com.android.server.uwb.util.ArrayUtils;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import com.google.common.primitives.Bytes;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Holds data of the UWB Regulatory Information according to FiRa BLE OOB v1.0
+ * specification.
+ */
+public class RegulatoryInfo {
+
+    private static final String LOG_TAG = RegulatoryInfo.class.getSimpleName();
+
+    // Minimum size of the full info
+    private static final int MIN_UWB_REGULATORY_INFO_SIZE = 9;
+
+    // The fields within UWB regulatory data
+    private static final int SOURCE_OF_INFO_FIELD_MASK = 0xF0;
+
+    private static final byte SOURCE_OF_INFO_USER_DEFINED_BITMASK = 0x8;
+    private static final byte SOURCE_OF_INFO_SATELLITE_NAVI_SYS_BITMASK = 0x4;
+    private static final byte SOURCE_OF_INFO_CELLULAR_SYS_BITMASK = 0x2;
+    private static final byte SOURCE_OF_INFO_ANOTHER_FIRA_DEVICE_BITMASK = 0x1;
+
+    private static final byte UWB_REGULATORY_INFO_RESERVED_FIELD_MASK = 0xE;
+    private static final byte UWB_REGULATORY_INFO_RESERVED_FIELD_DATA = 0x0;
+
+    private static final int OUTDOORS_TRANSMISSION_PERMITTED_FIELD_MASK = 0x1;
+    private static final int COUNTRY_CODE_FIELD_SIZE = 2;
+
+    private static final int CHANNEL_AND_POWER_FIELD_SIZE = 2;
+
+    /**
+     * Source of information of this regulatory info
+     */
+    public enum SourceOfInfo {
+        USER_DEFINED,
+        SATELLITE_NAVIGATION_SYSTEM,
+        CELLULAR_SYSTEM,
+        ANOTHER_FIRA_DEVICE,
+    }
+
+    public final SourceOfInfo sourceOfInfo;
+    public final boolean outdoorsTransmittionPermitted;
+    public final String countryCode;
+    public final int timestampSecondsSinceEpoch;
+    public final ChannelPowerInfo[] channelPowerInfos;
+
+    /**
+     * Generate the RegulatoryInfo from raw bytes array.
+     *
+     * @param bytes byte array containing the UWB regulatory data encoding based on the FiRa
+     *     specification.
+     * @return decode bytes into {@link RegulatoryInfo}, else null if invalid.
+     */
+    @Nullable
+    public static RegulatoryInfo fromBytes(@NonNull byte[] bytes) {
+        if (ArrayUtils.isEmpty(bytes)) {
+            logw("Failed to convert empty into UWB Regulatory Info.");
+            return null;
+        }
+
+        if (bytes.length < MIN_UWB_REGULATORY_INFO_SIZE) {
+            logw("Failed to convert bytes into UWB Regulatory Info due to invalid data size.");
+            return null;
+        }
+
+        ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byte firstByte = byteBuffer.get();
+
+        byte sourceOfInfoByte = (byte) ((firstByte & SOURCE_OF_INFO_FIELD_MASK) >> 4);
+        SourceOfInfo sourceOfInfo = parseSourceOfInfo(sourceOfInfoByte);
+
+        if ((firstByte & UWB_REGULATORY_INFO_RESERVED_FIELD_MASK)
+                != UWB_REGULATORY_INFO_RESERVED_FIELD_DATA) {
+            logw(
+                    "Failed to convert bytes into UWB Regulatory Info due to invalid"
+                            + " reserved field data.");
+            return null;
+        }
+
+        boolean outdoorsTransmittionPermitted =
+                (firstByte & OUTDOORS_TRANSMISSION_PERMITTED_FIELD_MASK) != 0;
+        byte[] countryCodeBytes = new byte[COUNTRY_CODE_FIELD_SIZE];
+        byteBuffer.get(countryCodeBytes);
+        String countryCode = new String(countryCodeBytes, StandardCharsets.UTF_8);
+
+        if (!UwbCountryCode.isValid(countryCode)) {
+            logw("Failed to convert bytes into UWB Regulatory Info due to invalid country code");
+            return null;
+        }
+
+        int timestampSecondsSinceEpoch = byteBuffer.getInt(); // Big-endian
+
+        int info_size =
+                1 + (bytes.length - MIN_UWB_REGULATORY_INFO_SIZE) / CHANNEL_AND_POWER_FIELD_SIZE;
+        List<ChannelPowerInfo> infos = new ArrayList<>();
+
+        for (int i = 0; i < info_size; i++) {
+            byte[] channelPowerInfoBytes = new byte[CHANNEL_AND_POWER_FIELD_SIZE];
+            byteBuffer.get(channelPowerInfoBytes);
+            ChannelPowerInfo info = ChannelPowerInfo.fromBytes(channelPowerInfoBytes);
+            if (info != null) {
+                infos.add(info);
+            }
+        }
+
+        return new RegulatoryInfo(
+                sourceOfInfo,
+                outdoorsTransmittionPermitted,
+                countryCode,
+                timestampSecondsSinceEpoch,
+                infos.toArray(new ChannelPowerInfo[0]));
+    }
+
+    /**
+     * Generate raw bytes array from RegulatoryInfo.
+     *
+     * @param info the UWB regulatory data.
+     * @return encoded bytes into byte array based on the FiRa specification.
+     */
+    public static byte[] toBytes(@NonNull RegulatoryInfo info) {
+        byte[] data =
+                new byte[] {
+                    (byte)
+                            (convertSourceOfInfo(info.sourceOfInfo)
+                                    | UWB_REGULATORY_INFO_RESERVED_FIELD_DATA
+                                    | convertOutdoorsTransmittionPermitted(
+                                            info.outdoorsTransmittionPermitted))
+                };
+        data =
+                Bytes.concat(
+                        data,
+                        info.countryCode.getBytes(StandardCharsets.UTF_8),
+                        DataTypeConversionUtil.i32ToByteArray(info.timestampSecondsSinceEpoch));
+        for (ChannelPowerInfo i : info.channelPowerInfos) {
+            data = Bytes.concat(data, ChannelPowerInfo.toBytes(i));
+        }
+        return data;
+    }
+
+    @Nullable
+    private static SourceOfInfo parseSourceOfInfo(byte sourceOfInfoByte) {
+        if (sourceOfInfoByte == 0) {
+            logw("Failed to parse 0 into Source Of Info.");
+            return null;
+        }
+        int count = 0;
+        SourceOfInfo info = SourceOfInfo.USER_DEFINED;
+        if ((sourceOfInfoByte & SOURCE_OF_INFO_USER_DEFINED_BITMASK) != 0) {
+            count += 1;
+            info = SourceOfInfo.USER_DEFINED;
+        }
+        if ((sourceOfInfoByte & SOURCE_OF_INFO_SATELLITE_NAVI_SYS_BITMASK) != 0) {
+            count += 1;
+            info = SourceOfInfo.SATELLITE_NAVIGATION_SYSTEM;
+        }
+        if ((sourceOfInfoByte & SOURCE_OF_INFO_CELLULAR_SYS_BITMASK) != 0) {
+            count += 1;
+            info = SourceOfInfo.CELLULAR_SYSTEM;
+        }
+        if ((sourceOfInfoByte & SOURCE_OF_INFO_ANOTHER_FIRA_DEVICE_BITMASK) != 0) {
+            count += 1;
+            info = SourceOfInfo.ANOTHER_FIRA_DEVICE;
+        }
+        if (count > 1) {
+            logw("Failed to parse multiple Source Of Info.");
+            return null;
+        }
+        return info;
+    }
+
+    private static byte convertSourceOfInfo(SourceOfInfo info) {
+        byte result = 0;
+        switch (info) {
+            case USER_DEFINED:
+                result = SOURCE_OF_INFO_USER_DEFINED_BITMASK;
+                break;
+            case SATELLITE_NAVIGATION_SYSTEM:
+                result = SOURCE_OF_INFO_SATELLITE_NAVI_SYS_BITMASK;
+                break;
+            case CELLULAR_SYSTEM:
+                result = SOURCE_OF_INFO_CELLULAR_SYS_BITMASK;
+                break;
+            case ANOTHER_FIRA_DEVICE:
+                result = SOURCE_OF_INFO_ANOTHER_FIRA_DEVICE_BITMASK;
+                break;
+        }
+        return (byte) ((result << 4) & SOURCE_OF_INFO_FIELD_MASK);
+    }
+
+    private static byte convertOutdoorsTransmittionPermitted(
+            boolean outdoorsTransmittionPermitted) {
+        return (byte)
+                ((outdoorsTransmittionPermitted ? 1 : 0)
+                        & OUTDOORS_TRANSMISSION_PERMITTED_FIELD_MASK);
+    }
+
+    public RegulatoryInfo(
+            SourceOfInfo sourceOfInfo,
+            boolean outdoorsTransmittionPermitted,
+            String countryCode,
+            int timestampSecondsSinceEpoch,
+            ChannelPowerInfo[] channelPowerInfos) {
+        this.sourceOfInfo = sourceOfInfo;
+        this.outdoorsTransmittionPermitted = outdoorsTransmittionPermitted;
+        this.countryCode = countryCode;
+        this.timestampSecondsSinceEpoch = timestampSecondsSinceEpoch;
+        this.channelPowerInfos = channelPowerInfos;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("RegulatoryInfo: SourceOfInfo=")
+                .append(sourceOfInfo)
+                .append(" OutdoorsTransmittionPermitted=")
+                .append(outdoorsTransmittionPermitted)
+                .append(" CountryCode=")
+                .append(countryCode)
+                .append(" TimestampSecondsSinceEpoch=")
+                .append(timestampSecondsSinceEpoch)
+                .append(" ")
+                .append(Arrays.toString(channelPowerInfos));
+        return sb.toString();
+    }
+
+    private static void logw(String log) {
+        Log.w(LOG_TAG, log);
+    }
+}
diff --git a/service/java/com/android/server/uwb/discovery/info/SecureComponentInfo.java b/service/java/com/android/server/uwb/discovery/info/SecureComponentInfo.java
new file mode 100644
index 0000000..d4b9c55
--- /dev/null
+++ b/service/java/com/android/server/uwb/discovery/info/SecureComponentInfo.java
@@ -0,0 +1,253 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.discovery.info;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import com.android.server.uwb.util.ArrayUtils;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * UWB Secure Component information according to FiRa BLE OOB v1.0 specification.
+ */
+public class SecureComponentInfo {
+    private static final String LOG_TAG = SecureComponentInfo.class.getSimpleName();
+
+    // The size of the full data
+    private static final int SECURE_COMPONENT_INFO_SIZE = 2;
+
+    private static final byte STATIC_INDICATION_BITMASK = (byte) 0x80;
+    private static final byte SECID_BITMASK = 0x7F;
+    private static final byte SECURE_COMPONENT_TYPE_BITMASK = (byte) 0xF0;
+    private static final byte SECURE_COMPONENT_PROTOCOL_TYPE_BITMASK = 0x0F;
+
+    // If this Secure Component is granted to be always available.
+    public final boolean staticIndication;
+    // SECID value (unsigned integer in the range 2..127, values 0 and 1 are reserved)
+    @IntRange(from = 2, to = 127)
+    public final int secid;
+
+    /**
+     * Type of Secure Component
+     */
+    public enum SecureComponentType {
+        // As defined by GloblePlatform
+        ESE_NONREMOVABLE(1),
+        // As defined by ETSI
+        UICC_REMOVABLE(2),
+        // As defined by GSMA
+        DISCRETE_EUICC_REMOVABLE(3),
+        // As defined by GSMA
+        DISCRETE_EUICC_NONREMOVABLE(4),
+        // As defined by GSMA
+        INTEGRATED_EUICC_NONREMOVABLE(5),
+        // Software emulated SC (e.g. Android HCE)
+        SW_EMULATED_SC(6),
+
+        VENDOR_PROPRIETARY(15);
+
+        private final int mValue;
+        private static Map sMap = new HashMap<>();
+
+        SecureComponentType(int value) {
+            this.mValue = value;
+        }
+
+        static {
+            for (SecureComponentType type : SecureComponentType.values()) {
+                sMap.put(type.mValue, type);
+            }
+        }
+
+        /**
+         * Get the SecureComponentType based on the given value.
+         *
+         * @param value type value defined by FiRa.
+         * @return {@link SecureComponentType} associated with the value, else null if invalid.
+         */
+        @Nullable
+        public static SecureComponentType valueOf(int value) {
+            return (SecureComponentType) sMap.get(value);
+        }
+
+        public int getValue() {
+            return mValue;
+        }
+    }
+
+    public final SecureComponentType secureComponentType;
+
+    /**
+     * Type of Secure Component Protocol
+     */
+    public enum SecureComponentProtocolType {
+        // As defined by FiRa
+        FIRA_OOB_ADMINISTRATIVE_PROTOCOL(1),
+        // As defined by ISO/IEC 7816-4
+        ISO_IEC_7816_4(2),
+
+        VENDOR_PROPRIETARY(15);
+
+        private final int mValue;
+        private static Map sMap = new HashMap<>();
+
+        SecureComponentProtocolType(int value) {
+            this.mValue = value;
+        }
+
+        static {
+            for (SecureComponentProtocolType type : SecureComponentProtocolType.values()) {
+                sMap.put(type.mValue, type);
+            }
+        }
+
+        /**
+         * Get the SecureComponentProtocolType based on the given value.
+         *
+         * @param value type value defined by FiRa.
+         * @return {@link SecureComponentProtocolType} associated with the value, else null if
+         *     invalid.
+         */
+        @Nullable
+        public static SecureComponentProtocolType valueOf(int value) {
+            return (SecureComponentProtocolType) sMap.get(value);
+        }
+
+        public int getValue() {
+            return mValue;
+        }
+    }
+
+    public final SecureComponentProtocolType secureComponentProtocolType;
+
+    /**
+     * Generate the SecureComponentInfo from raw bytes array.
+     *
+     * @param bytes byte array containing the Secure Component info as part of the UWB indication
+     *     data. Data encoding based on the FiRa specification.
+     * @return decode bytes into {@link SecureComponentInfo}.
+     */
+    public static SecureComponentInfo fromBytes(@NonNull byte[] bytes) {
+        if (ArrayUtils.isEmpty(bytes)) {
+            logw("Failed to convert empty into UWB Secure Component info.");
+            return null;
+        }
+
+        if (bytes.length < SECURE_COMPONENT_INFO_SIZE) {
+            logw(
+                    "Failed to convert bytes into UWB Secure Component info due to invalid data"
+                            + " size.");
+            return null;
+        }
+
+        boolean staticIndication = (bytes[0] & STATIC_INDICATION_BITMASK) != 0;
+        int secid = (int) (bytes[0] & SECID_BITMASK);
+        if (secid < 2 || secid > 127) {
+            logw("Failed to convert bytes into UWB Secure Component info due to invalid secid");
+            return null;
+        }
+        SecureComponentType type =
+                SecureComponentType.valueOf(
+                        (int) ((bytes[1] & SECURE_COMPONENT_TYPE_BITMASK) >>> 4));
+        if (type == null) {
+            logw(
+                    "Failed to convert bytes into UWB Secure Component info due to invalid Secure"
+                            + " Component Type");
+            return null;
+        }
+        SecureComponentProtocolType protocolType =
+                SecureComponentProtocolType.valueOf(
+                        (int) ((bytes[1] & SECURE_COMPONENT_PROTOCOL_TYPE_BITMASK)));
+        if (protocolType == null) {
+            logw(
+                    "Failed to convert bytes into UWB Secure Component info due to invalid Secure"
+                            + " Component Protocol Type");
+            return null;
+        }
+
+        return new SecureComponentInfo(staticIndication, secid, type, protocolType);
+    }
+
+    /**
+     * Generate raw bytes array from SecureComponentInfo.
+     *
+     * @param info the Secure Component info.
+     * @return encoded bytes into byte array based on the FiRa specification.
+     */
+    public static byte[] toBytes(@NonNull SecureComponentInfo info) {
+        return new byte[] {
+            (byte) (convertStaticIndication(info.staticIndication) | convertSedid(info.secid)),
+            (byte)
+                    (convertsecureComponentType(info.secureComponentType)
+                            | convertSecureComponentProtocolType(info.secureComponentProtocolType))
+        };
+    }
+
+    private static byte convertStaticIndication(boolean staticIndication) {
+        return (byte) (((staticIndication ? 1 : 0) << 7) & STATIC_INDICATION_BITMASK);
+    }
+
+    private static byte convertSedid(int secid) {
+        return (byte) (DataTypeConversionUtil.i32ToByteArray(secid)[3] & SECID_BITMASK);
+    }
+
+    private static byte convertsecureComponentType(SecureComponentType type) {
+        return (byte)
+                ((DataTypeConversionUtil.i32ToByteArray(type.getValue())[3] << 4)
+                        & SECURE_COMPONENT_TYPE_BITMASK);
+    }
+
+    private static byte convertSecureComponentProtocolType(SecureComponentProtocolType type) {
+        return (byte)
+                (DataTypeConversionUtil.i32ToByteArray(type.getValue())[3]
+                        & SECURE_COMPONENT_PROTOCOL_TYPE_BITMASK);
+    }
+
+    public SecureComponentInfo(
+            boolean staticIndication,
+            int secid,
+            SecureComponentType secureComponentType,
+            SecureComponentProtocolType secureComponentProtocolType) {
+        this.staticIndication = staticIndication;
+        this.secid = secid;
+        this.secureComponentType = secureComponentType;
+        this.secureComponentProtocolType = secureComponentProtocolType;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("SecureComponentInfo: staticIndication=")
+                .append(staticIndication)
+                .append(" secid=")
+                .append(secid)
+                .append(" secureComponentType=")
+                .append(secureComponentType)
+                .append(" secureComponentProtocolType=")
+                .append(secureComponentProtocolType);
+        return sb.toString();
+    }
+
+    private static void logw(String log) {
+        Log.w(LOG_TAG, log);
+    }
+}
diff --git a/service/java/com/android/server/uwb/discovery/info/UwbIndicationData.java b/service/java/com/android/server/uwb/discovery/info/UwbIndicationData.java
new file mode 100644
index 0000000..f511181
--- /dev/null
+++ b/service/java/com/android/server/uwb/discovery/info/UwbIndicationData.java
@@ -0,0 +1,218 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.discovery.info;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import com.android.server.uwb.util.ArrayUtils;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import com.google.common.primitives.Bytes;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Holds the UWB Indication Data according to FiRa BLE OOB v1.0 specification.
+ */
+public class UwbIndicationData {
+
+    private static final String LOG_TAG = UwbIndicationData.class.getSimpleName();
+
+    // Minimum size of the full data
+    private static final int UWB_INDICATION_DATA_SIZE = 2;
+
+    // The capabilities field within UWB indication data
+    private static final byte FIRA_UWB_SUPPORT_BITMASK = (byte) 0x80;
+    private static final byte ISO14443_SUPPORT_BITMASK = 0x40;
+    private static final byte UWB_REGULATORY_INFO_AVAILABLE_IN_AD_BITMASK = 0x20;
+    private static final byte UWB_REGULATORY_INFO_AVAILABLE_IN_OOB_BITMASK = 0x10;
+    private static final byte FIRA_PROFILE_INFO_AVAILABLE_IN_AD_BITMASK = 0x08;
+    private static final byte FIRA_PROFILE_INFO_AVAILABLE_IN_OOB_BITMASK = 0x04;
+    private static final byte CAPABILITIES_RESERVED_FIELD_BITMASK = 0x02;
+    private static final byte CAPABILITIES_RESERVED_FIELD_DATA = 0x0;
+    private static final byte DUAL_GAP_ROLE_SUPPORT_BITMASK = 0x01;
+
+    // Elements of the secure component field list within UWB indication data.
+    private static final int SECURE_COMPONENT_ELEMENT_SIZE = 2;
+
+    public final boolean firaUwbSupport;
+    public final boolean iso14443Support;
+    public final boolean uwbRegulartoryInfoAvailableInAd;
+    public final boolean uwbRegulartoryInfoAvailableInOob;
+    public final boolean firaProfileInfoAvailableInAd;
+    public final boolean firaProfileInfoAvailableInOob;
+    public final boolean dualGapRoleSupport;
+    public final int bluetoothRssiThresholdDbm;
+    public final SecureComponentInfo[] secureComponentInfos;
+
+    /**
+     * Generate the UwbIndicationData from raw bytes array.
+     *
+     * @param bytes byte array containing the UWB Indication Data encoding based on the FiRa
+     *     specification.
+     * @return decode bytes into {@link UwbIndicationData}, else null if invalid.
+     */
+    @Nullable
+    public static UwbIndicationData fromBytes(@NonNull byte[] bytes) {
+        if (ArrayUtils.isEmpty(bytes)) {
+            logw("Failed to convert empty into UWB Indication Data.");
+            return null;
+        }
+
+        if (bytes.length < UWB_INDICATION_DATA_SIZE) {
+            logw("Failed to convert bytes into UWB Indication Data due to invalid data size.");
+            return null;
+        }
+
+        ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+
+        byte uwbCapabilities = byteBuffer.get();
+        boolean firaUwbSupport = (uwbCapabilities & FIRA_UWB_SUPPORT_BITMASK) != 0;
+        boolean iso14443Support = (uwbCapabilities & ISO14443_SUPPORT_BITMASK) != 0;
+        boolean uwbRegulartoryInfoAvailableInAd =
+                (uwbCapabilities & UWB_REGULATORY_INFO_AVAILABLE_IN_AD_BITMASK) != 0;
+        boolean uwbRegulartoryInfoAvailableInOob =
+                (uwbCapabilities & UWB_REGULATORY_INFO_AVAILABLE_IN_OOB_BITMASK) != 0;
+        boolean firaProfileInfoAvailableInAd =
+                (uwbCapabilities & FIRA_PROFILE_INFO_AVAILABLE_IN_AD_BITMASK) != 0;
+        boolean firaProfileInfoAvailableInOob =
+                (uwbCapabilities & FIRA_PROFILE_INFO_AVAILABLE_IN_OOB_BITMASK) != 0;
+        boolean dualGapRoleSupport = (uwbCapabilities & DUAL_GAP_ROLE_SUPPORT_BITMASK) != 0;
+        byte capabilitiesReservedField =
+                (byte) (uwbCapabilities & CAPABILITIES_RESERVED_FIELD_BITMASK);
+        if (capabilitiesReservedField != CAPABILITIES_RESERVED_FIELD_DATA) {
+            logw(
+                    "Failed to convert bytes into UWB Indication Data due to reserved field in uwb"
+                            + " capabilities is unmatched");
+            return null;
+        }
+
+        int bluetoothRssiThresholdDbm = (int) byteBuffer.get();
+
+        int info_size = (bytes.length - UWB_INDICATION_DATA_SIZE) / SECURE_COMPONENT_ELEMENT_SIZE;
+        List<SecureComponentInfo> infos = new ArrayList<>();
+
+        for (int i = 0; i < info_size; i++) {
+            byte[] secureComponentInfoBytes = new byte[SECURE_COMPONENT_ELEMENT_SIZE];
+            byteBuffer.get(secureComponentInfoBytes);
+            SecureComponentInfo info = SecureComponentInfo.fromBytes(secureComponentInfoBytes);
+            if (info != null) {
+                infos.add(info);
+            }
+        }
+
+        return new UwbIndicationData(
+                firaUwbSupport,
+                iso14443Support,
+                uwbRegulartoryInfoAvailableInAd,
+                uwbRegulartoryInfoAvailableInOob,
+                firaProfileInfoAvailableInAd,
+                firaProfileInfoAvailableInOob,
+                dualGapRoleSupport,
+                bluetoothRssiThresholdDbm,
+                infos.toArray(new SecureComponentInfo[0]));
+    }
+
+    /**
+     * Generate raw bytes array from UwbIndicationData.
+     *
+     * @param info the UWB Indication Data
+     * @return encoded bytes into byte array based on the FiRa specification.
+     */
+    public static byte[] toBytes(@NonNull UwbIndicationData info) {
+        byte[] data =
+                new byte[] {
+                    convertCapabilitiesField(info),
+                    DataTypeConversionUtil.i32ToByteArray(info.bluetoothRssiThresholdDbm)[3]
+                };
+        for (SecureComponentInfo i : info.secureComponentInfos) {
+            data = Bytes.concat(data, SecureComponentInfo.toBytes(i));
+        }
+        return data;
+    }
+
+    private static byte convertCapabilitiesField(@NonNull UwbIndicationData info) {
+        return (byte)
+                ((((info.firaUwbSupport ? 1 : 0) << 7) & FIRA_UWB_SUPPORT_BITMASK)
+                        | (((info.iso14443Support ? 1 : 0) << 6) & ISO14443_SUPPORT_BITMASK)
+                        | (((info.uwbRegulartoryInfoAvailableInAd ? 1 : 0) << 5)
+                                & UWB_REGULATORY_INFO_AVAILABLE_IN_AD_BITMASK)
+                        | (((info.uwbRegulartoryInfoAvailableInOob ? 1 : 0) << 4)
+                                & UWB_REGULATORY_INFO_AVAILABLE_IN_OOB_BITMASK)
+                        | (((info.firaProfileInfoAvailableInAd ? 1 : 0) << 3)
+                                & FIRA_PROFILE_INFO_AVAILABLE_IN_AD_BITMASK)
+                        | (((info.firaProfileInfoAvailableInOob ? 1 : 0) << 2)
+                                & FIRA_PROFILE_INFO_AVAILABLE_IN_OOB_BITMASK)
+                        | ((CAPABILITIES_RESERVED_FIELD_DATA << 1)
+                                & CAPABILITIES_RESERVED_FIELD_BITMASK)
+                        | ((info.dualGapRoleSupport ? 1 : 0) & DUAL_GAP_ROLE_SUPPORT_BITMASK));
+    }
+
+    public UwbIndicationData(
+            boolean firaUwbSupport,
+            boolean iso14443Support,
+            boolean uwbRegulartoryInfoAvailableInAd,
+            boolean uwbRegulartoryInfoAvailableInOob,
+            boolean firaProfileInfoAvailableInAd,
+            boolean firaProfileInfoAvailableInOob,
+            boolean dualGapRoleSupport,
+            int bluetoothRssiThresholdDbm,
+            SecureComponentInfo[] secureComponentInfos) {
+        this.firaUwbSupport = firaUwbSupport;
+        this.iso14443Support = iso14443Support;
+        this.uwbRegulartoryInfoAvailableInAd = uwbRegulartoryInfoAvailableInAd;
+        this.uwbRegulartoryInfoAvailableInOob = uwbRegulartoryInfoAvailableInOob;
+        this.firaProfileInfoAvailableInAd = firaProfileInfoAvailableInAd;
+        this.firaProfileInfoAvailableInOob = firaProfileInfoAvailableInOob;
+        this.dualGapRoleSupport = dualGapRoleSupport;
+        this.bluetoothRssiThresholdDbm = bluetoothRssiThresholdDbm;
+        this.secureComponentInfos = secureComponentInfos;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("UwbIndicationData: firaUwbSupport=")
+                .append(firaUwbSupport)
+                .append(" iso14443Support=")
+                .append(iso14443Support)
+                .append(" uwbRegulartoryInfoAvailableInAd=")
+                .append(uwbRegulartoryInfoAvailableInAd)
+                .append(" uwbRegulartoryInfoAvailableInOob=")
+                .append(uwbRegulartoryInfoAvailableInOob)
+                .append(" firaProfileInfoAvailableInAd=")
+                .append(firaProfileInfoAvailableInAd)
+                .append(" firaProfileInfoAvailableInOob=")
+                .append(firaProfileInfoAvailableInOob)
+                .append(" dualGapRoleSupport=")
+                .append(dualGapRoleSupport)
+                .append(" bluetoothRssiThresholdDbm=")
+                .append(bluetoothRssiThresholdDbm)
+                .append(" ")
+                .append(Arrays.toString(secureComponentInfos));
+        return sb.toString();
+    }
+
+    private static void logw(String log) {
+        Log.w(LOG_TAG, log);
+    }
+}
diff --git a/service/java/com/android/server/uwb/discovery/info/VendorSpecificData.java b/service/java/com/android/server/uwb/discovery/info/VendorSpecificData.java
new file mode 100644
index 0000000..0f1f3bb
--- /dev/null
+++ b/service/java/com/android/server/uwb/discovery/info/VendorSpecificData.java
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.discovery.info;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import com.android.server.uwb.util.ArrayUtils;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import com.google.common.primitives.Bytes;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+/**
+ * Holds FiRa UWB vendor specific data according to FiRa BLE OOB v1.0 specification.
+ */
+public class VendorSpecificData {
+    private static final String LOG_TAG = VendorSpecificData.class.getSimpleName();
+
+    private static final int VENDOR_ID_FIELD_SIZE = 2;
+
+    // Minimum size of the full info
+    private static final int MIN_VENDOR_SPECIFIC_DATA_SIZE = VENDOR_ID_FIELD_SIZE;
+
+    // Vendor ID as assigned by Bluetooth SIG.
+    public final int vendorId;
+    // Data encoded with vendor specific encoding.
+    public final byte[] vendorData;
+
+    /**
+     * Generate the VendorSpecificData from raw bytes array.
+     *
+     * @param bytes byte array containing the UWB vendor specific data.
+     * @return decode bytes into {@link VendorSpecificData}, else null if invalid.
+     */
+    @Nullable
+    public static VendorSpecificData fromBytes(@NonNull byte[] bytes) {
+        if (ArrayUtils.isEmpty(bytes)) {
+            logw("Failed to convert empty into UWB vendor specific data.");
+            return null;
+        }
+
+        if (bytes.length < MIN_VENDOR_SPECIFIC_DATA_SIZE) {
+            logw("Failed to convert bytes into UWB vendor specific data due to invalid data size.");
+            return null;
+        }
+
+        ByteBuffer buffer = ByteBuffer.wrap(bytes);
+        int vendorId = buffer.order(ByteOrder.LITTLE_ENDIAN).getShort();
+
+        byte[] vendorData = new byte[buffer.remaining()];
+        buffer.order(ByteOrder.BIG_ENDIAN).get(vendorData);
+
+        return new VendorSpecificData(vendorId, vendorData);
+    }
+
+    /**
+     * Generate raw bytes array from VendorSpecificData.
+     *
+     * @param info the UWB vendor specific data.
+     * @return encoded bytes into byte array based on the FiRa specification.
+     */
+    public static byte[] toBytes(@NonNull VendorSpecificData info) {
+        byte[] id = DataTypeConversionUtil.i32ToLeByteArray(info.vendorId);
+        return Bytes.concat(new byte[] {id[0], id[1]}, info.vendorData);
+    }
+
+    public VendorSpecificData(@IntRange(from = 0, to = 65535) int vendorId, byte[] vendorData) {
+        this.vendorId = vendorId;
+        this.vendorData = vendorData;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("VendorSpecificData: VendorId=")
+                .append(vendorId)
+                .append(" VendorSpecificData=")
+                .append(Arrays.toString(vendorData));
+        return sb.toString();
+    }
+
+    private static void logw(String log) {
+        Log.w(LOG_TAG, log);
+    }
+}
diff --git a/service/java/com/android/server/uwb/info/UwbPowerStats.java b/service/java/com/android/server/uwb/info/UwbPowerStats.java
new file mode 100644
index 0000000..e974e3c
--- /dev/null
+++ b/service/java/com/android/server/uwb/info/UwbPowerStats.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.info;
+
+/**
+ * Power related status reported by the UWB subsystem.
+ * All values should never decrease after the start of subsystem.
+ */
+public class UwbPowerStats {
+    private static final String TAG = UwbPowerStats.class.getSimpleName();
+
+    /**
+     * The duration of UWB operating in the Tx mode in millis.
+     * This may include time for HW configuration, ramp up and down.
+     */
+    private int mTxTimeMs;
+
+    /**
+     * The duration of UWB operating in the Rx mode in millis.
+     * This may include time for HW configuration and listen mode.
+     */
+    private int mRxTimeMs;
+
+    /**
+     * The duration of UWB operating in the idle mode (neither Tx nor Rx).
+     * For the HW with very low idle current, it may not be meaningful to maintain this
+     * count and thus the value could be always zero.
+     */
+    private int mIdleTimeMs;
+
+    /**
+     * Total count of host wakeup due to UWB subsystem event.
+     */
+    private int mTotalWakeCount;
+
+    public UwbPowerStats(int txTimeMs, int rxTimeMs, int idleTimeMs, int totalWakeCount) {
+        mTxTimeMs = txTimeMs;
+        mRxTimeMs = rxTimeMs;
+        mIdleTimeMs = idleTimeMs;
+        mTotalWakeCount = totalWakeCount;
+    }
+
+    /**
+     * get total Tx time in millis
+     */
+    public int getTxTimeMs() {
+        return mTxTimeMs;
+    }
+
+    /**
+     * get total Rx time in millis
+     */
+    public int getRxTimeMs() {
+        return mRxTimeMs;
+    }
+
+    /**
+     * get total idle time in millis
+     */
+    public int getIdleTimeMs() {
+        return mIdleTimeMs;
+    }
+
+    /**
+     * get total wakeup count
+     */
+    public int getTotalWakeCount() {
+        return mTotalWakeCount;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("UwbPowerStats: tx_time_ms=").append(mTxTimeMs)
+                .append(" rx_time_ms=").append(mRxTimeMs)
+                .append(" idle_time_ms=").append(mIdleTimeMs)
+                .append(" total_wake_count=").append(mTotalWakeCount);
+        return sb.toString();
+    }
+}
diff --git a/service/java/com/android/server/uwb/jni/INativeUwbManager.java b/service/java/com/android/server/uwb/jni/INativeUwbManager.java
new file mode 100644
index 0000000..ae6820d
--- /dev/null
+++ b/service/java/com/android/server/uwb/jni/INativeUwbManager.java
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.jni;
+
+import android.os.RemoteException;
+
+import com.android.server.uwb.data.UwbMulticastListUpdateStatus;
+import com.android.server.uwb.data.UwbRangingData;
+/*import com.android.server.uwb.test.UwbTestLoopBackTestResult;
+import com.android.server.uwb.test.UwbTestPeriodicTxResult;
+import com.android.server.uwb.test.UwbTestRxPacketErrorRateResult;
+import com.android.server.uwb.test.UwbTestRxResult;*/
+
+public interface INativeUwbManager {
+    /**
+     * Notifies transaction
+     */
+    interface SessionNotification {
+        /**
+         * Interface for receiving Ranging Data Notification
+         *
+         * @param rangingData : refer to UCI GENERIC SPECIFICATION Table 22:Ranging Data
+         *                    Notification
+         */
+        void onRangeDataNotificationReceived(UwbRangingData rangingData);
+
+        /**
+         * Interface for receiving Session Status Notification
+         *
+         * @param id         : Session ID
+         * @param state      : Session State
+         * @param reasonCode : Reason Code - UCI GENERIC SPECIFICATION Table 15 : state change with
+         *                   reason codes
+         */
+        void onSessionStatusNotificationReceived(long id, int state, int reasonCode);
+
+        /**
+         * Interface for receiving Multicast List Update Data
+         *
+         * @param multicastListUpdateData : refer to SESSION_UPDATE_CONTROLLER_MULTICAST_LIST_NTF
+         */
+        void onMulticastListUpdateNotificationReceived(
+                UwbMulticastListUpdateStatus multicastListUpdateData);
+    }
+
+    interface DeviceNotification {
+        /**
+         * Interface for receiving Device Status Notification
+         *
+         * @param state : refer to UCI GENERIC SPECIFICATION Table 9: Device Status Notification
+         */
+        void onDeviceStatusNotificationReceived(int state);
+
+        /**
+         * Interface for receiving Control Message for Generic Error
+         *
+         * @param status : refer to UCI GENERIC SPECIFICATION Table 12: Control Message for Generic
+         *               Error
+         */
+        void onCoreGenericErrorNotificationReceived(int status);
+    }
+
+    interface VendorNotification {
+        /**
+         * Interface for receiving Vendor UCI notifications.
+         */
+        void onVendorUciNotificationReceived(int gid, int oid, byte[] payload)
+                throws RemoteException;
+    }
+    /* Unused now */
+    /*interface RfTestNotification {
+        void onPeriodicTxDataNotificationReceived(UwbTestPeriodicTxResult periodicTxData);
+        void onPerRxDataNotificationReceived(UwbTestRxPacketErrorRateResult perRxData);
+        void onLoopBackTestDataNotificationReceived(UwbTestLoopBackTestResult uwbLoopBackData);
+        void onRxTestDataNotificationReceived(UwbTestRxResult rxData);
+    }*/
+}
diff --git a/service/java/com/android/server/uwb/jni/NativeUwbManager.java b/service/java/com/android/server/uwb/jni/NativeUwbManager.java
new file mode 100644
index 0000000..4ad8e24
--- /dev/null
+++ b/service/java/com/android/server/uwb/jni/NativeUwbManager.java
@@ -0,0 +1,351 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.jni;
+
+import android.annotation.NonNull;
+import android.util.Log;
+
+import com.android.server.uwb.UwbInjector;
+import com.android.server.uwb.data.UwbConfigStatusData;
+import com.android.server.uwb.data.UwbMulticastListUpdateStatus;
+import com.android.server.uwb.data.UwbRangingData;
+import com.android.server.uwb.data.UwbTlvData;
+import com.android.server.uwb.data.UwbUciConstants;
+import com.android.server.uwb.data.UwbVendorUciResponse;
+import com.android.server.uwb.info.UwbPowerStats;
+
+public class NativeUwbManager {
+    private static final String TAG = NativeUwbManager.class.getSimpleName();
+
+    public final Object mSessionFnLock = new Object();
+    public final Object mSessionCountFnLock = new Object();
+    public final Object mGlobalStateFnLock = new Object();
+    public final Object mGetSessionStatusFnLock = new Object();
+    public final Object mSetAppConfigFnLock = new Object();
+    private final UwbInjector mUwbInjector;
+    protected INativeUwbManager.DeviceNotification mDeviceListener;
+    protected INativeUwbManager.SessionNotification mSessionListener;
+    private long mDispatcherPointer;
+    protected INativeUwbManager.VendorNotification mVendorListener;
+
+    public NativeUwbManager(@NonNull UwbInjector uwbInjector) {
+        mUwbInjector = uwbInjector;
+        loadLibrary();
+    }
+
+    protected void loadLibrary() {
+        System.loadLibrary("uwb_uci_jni_rust");
+        nativeInit();
+    }
+
+    public void setDeviceListener(INativeUwbManager.DeviceNotification deviceListener) {
+        mDeviceListener = deviceListener;
+    }
+
+    public void setSessionListener(INativeUwbManager.SessionNotification sessionListener) {
+        mSessionListener = sessionListener;
+    }
+
+    public void setVendorListener(INativeUwbManager.VendorNotification vendorListener) {
+        mVendorListener = vendorListener;
+    }
+
+    public void onDeviceStatusNotificationReceived(int deviceState) {
+        Log.d(TAG, "onDeviceStatusNotificationReceived(" + deviceState + ")");
+        mDeviceListener.onDeviceStatusNotificationReceived(deviceState);
+    }
+
+    public void onCoreGenericErrorNotificationReceived(int status) {
+        Log.d(TAG, "onCoreGenericErrorNotificationReceived(" + status + ")");
+        mDeviceListener.onCoreGenericErrorNotificationReceived(status);
+    }
+
+    public void onSessionStatusNotificationReceived(long id, int state, int reasonCode) {
+        Log.d(TAG, "onSessionStatusNotificationReceived(" + id + ", " + state + ", " + reasonCode
+                + ")");
+        mSessionListener.onSessionStatusNotificationReceived(id, state, reasonCode);
+    }
+
+    public void onRangeDataNotificationReceived(UwbRangingData rangeData) {
+        Log.d(TAG, "onRangeDataNotificationReceived : " + rangeData);
+        mSessionListener.onRangeDataNotificationReceived(rangeData);
+    }
+
+    public void onMulticastListUpdateNotificationReceived(
+            UwbMulticastListUpdateStatus multicastListUpdateData) {
+        Log.d(TAG, "onMulticastListUpdateNotificationReceived : " + multicastListUpdateData);
+        mSessionListener.onMulticastListUpdateNotificationReceived(multicastListUpdateData);
+    }
+
+    /**
+     * Enable UWB hardware.
+     *
+     * @return : If this returns true, UWB is on
+     */
+    public synchronized boolean doInitialize() {
+        if (this.mDispatcherPointer == 0L) {
+            this.mDispatcherPointer = nativeDispatcherNew();
+        }
+        return nativeDoInitialize();
+    }
+
+    /**
+     * Disable UWB hardware.
+     *
+     * @return : If this returns true, UWB is off
+     */
+    public synchronized boolean doDeinitialize() {
+        nativeDoDeinitialize();
+        nativeDispatcherDestroy();
+        this.mDispatcherPointer = 0L;
+        return true;
+    }
+
+    public synchronized long getTimestampResolutionNanos() {
+        return 0L;
+        /* TODO: Not Implemented in native stack
+        return nativeGetTimestampResolutionNanos(); */
+    }
+
+    /**
+     * Retrieves maximum number of UWB sessions concurrently
+     *
+     * @return : Retrieves maximum number of UWB sessions concurrently
+     */
+    public int getMaxSessionNumber() {
+        return nativeGetMaxSessionNumber();
+    }
+
+    /**
+     * Retrieves power related stats
+     *
+     */
+    public UwbPowerStats getPowerStats() {
+        return nativeGetPowerStats();
+    }
+
+    /**
+     * Creates the new UWB session with parameter session ID and type of the session.
+     *
+     * @param sessionId   : Session ID is 4 Octets unique random number generated by application
+     * @param sessionType : Type of session 0x00: Ranging session 0x01: Data transfer 0x02-0x9F: RFU
+     *                    0xA0-0xCF: Reserved for Vendor Specific use case 0xD0: Device Test Mode
+     *                    0xD1-0xDF: RFU 0xE0-0xFF: Vendor Specific use
+     * @return : {@link UwbUciConstants}  Status code
+     */
+    public byte initSession(int sessionId, byte sessionType) {
+        synchronized (mSessionFnLock) {
+            return nativeSessionInit(sessionId, sessionType);
+        }
+    }
+
+    /**
+     * De-initializes the session.
+     *
+     * @param sessionId : Session ID for which session to be de-initialized
+     * @return : {@link UwbUciConstants}  Status code
+     */
+    public byte deInitSession(int sessionId) {
+        synchronized (mSessionFnLock) {
+            return nativeSessionDeInit(sessionId);
+        }
+    }
+
+    /**
+     * reset the UWBs
+     *
+     * @param resetConfig : Reset config
+     * @return : {@link UwbUciConstants}  Status code
+     */
+    public byte resetDevice(byte resetConfig) {
+        return nativeResetDevice(resetConfig);
+    }
+
+    /**
+     * Retrieves number of UWB sessions in the UWBS.
+     *
+     * @return : Number of UWB sessions present in the UWBS.
+     */
+    public byte getSessionCount() {
+        synchronized (mSessionCountFnLock) {
+            return nativeGetSessionCount();
+        }
+    }
+
+    /**
+     * Queries the current state of the UWB session.
+     *
+     * @param sessionId : Session of the UWB session for which current session state to be queried
+     * @return : {@link UwbUciConstants}  Session State
+     */
+    public byte getSessionState(int sessionId) {
+        synchronized (mGetSessionStatusFnLock) {
+            return nativeGetSessionState(sessionId);
+        }
+    }
+
+    /**
+     * Starts a UWB session.
+     *
+     * @param sessionId : Session ID for which ranging shall start
+     * @return : {@link UwbUciConstants}  Status code
+     */
+    public byte startRanging(int sessionId) {
+        synchronized (mSessionFnLock) {
+            return nativeRangingStart(sessionId);
+        }
+    }
+
+    /**
+     * Stops the ongoing UWB session.
+     *
+     * @param sessionId : Stop the requested ranging session.
+     * @return : {@link UwbUciConstants}  Status code
+     */
+    public byte stopRanging(int sessionId) {
+        synchronized (mSessionFnLock) {
+            return nativeRangingStop(sessionId);
+        }
+    }
+
+    /**
+     * set APP Configuration Parameters for the requested UWB session
+     *
+     * @param noOfParams        : The number (n) of APP Configuration Parameters
+     * @param appConfigParamLen : The length of APP Configuration Parameters
+     * @param appConfigParams   : APP Configuration Parameter
+     * @return : {@link UwbConfigStatusData} : Contains statuses for all cfg_id
+     */
+    public UwbConfigStatusData setAppConfigurations(int sessionId, int noOfParams,
+            int appConfigParamLen, byte[] appConfigParams) {
+        synchronized (mSetAppConfigFnLock) {
+            return nativeSetAppConfigurations(sessionId, noOfParams, appConfigParamLen,
+                    appConfigParams);
+        }
+    }
+
+    /**
+     * Get APP Configuration Parameters for the requested UWB session
+     *
+     * @param noOfParams        : The number (n) of APP Configuration Parameters
+     * @param appConfigParamLen : The length of APP Configuration Parameters
+     * @param appConfigIds      : APP Configuration Parameter
+     * @return :  {@link UwbTlvData} : All tlvs that are to be decoded
+     */
+    public UwbTlvData getAppConfigurations(int sessionId, int noOfParams, int appConfigParamLen,
+            byte[] appConfigIds) {
+        synchronized (mSetAppConfigFnLock) {
+            return nativeGetAppConfigurations(sessionId, noOfParams, appConfigParamLen,
+                    appConfigIds);
+        }
+    }
+
+    /**
+     * Get Core Capabilities information
+     *
+     * @return :  {@link UwbTlvData} : All tlvs that are to be decoded
+     */
+    public UwbTlvData getCapsInfo() {
+        synchronized (mGlobalStateFnLock) {
+            return nativeGetCapsInfo();
+        }
+    }
+
+    /**
+     * Update Multicast list for the requested UWB session
+     *
+     * @param sessionId  : Session ID to which multicast list to be updated
+     * @param action     : Update the multicast list by adding or removing
+     *                     0x00 - Adding
+     *                     0x01 - removing
+     * @param noOfControlee : The number(n) of Controlees
+     * @param addresses     : address list of Controlees
+     * @param subSessionIds : Specific sub-session ID list of Controlees
+     * @return : refer to SESSION_SET_APP_CONFIG_RSP
+     * in the Table 16: Control messages to set Application configurations
+     */
+    public byte controllerMulticastListUpdate(int sessionId, int action, int noOfControlee,
+            short[] addresses, int[]subSessionIds) {
+        synchronized (mSessionFnLock) {
+            return nativeControllerMulticastListUpdate(sessionId, (byte) action,
+                    (byte) noOfControlee, addresses, subSessionIds);
+        }
+    }
+
+    /**
+     * Set country code.
+     *
+     * @param countryCode 2 char ISO country code
+     */
+    public byte setCountryCode(byte[] countryCode) {
+        Log.i(TAG, "setCountryCode: " + new String(countryCode));
+        synchronized (mGlobalStateFnLock) {
+            return nativeSetCountryCode(countryCode);
+        }
+    }
+
+    @NonNull
+    public UwbVendorUciResponse sendRawVendorCmd(int gid, int oid, byte[] payload) {
+        synchronized (mGlobalStateFnLock) {
+            return nativeSendRawVendorCmd(gid, oid, payload);
+        }
+    }
+
+    private native long nativeDispatcherNew();
+
+    private native void nativeDispatcherDestroy();
+
+    private native boolean nativeInit();
+
+    private native boolean nativeDoInitialize();
+
+    private native boolean nativeDoDeinitialize();
+
+    private native long nativeGetTimestampResolutionNanos();
+
+    private native UwbPowerStats nativeGetPowerStats();
+
+    private native int nativeGetMaxSessionNumber();
+
+    private native byte nativeResetDevice(byte resetConfig);
+
+    private native byte nativeSessionInit(int sessionId, byte sessionType);
+
+    private native byte nativeSessionDeInit(int sessionId);
+
+    private native byte nativeGetSessionCount();
+
+    private native byte nativeRangingStart(int sessionId);
+
+    private native byte nativeRangingStop(int sessionId);
+
+    private native byte nativeGetSessionState(int sessionId);
+
+    private native UwbConfigStatusData nativeSetAppConfigurations(int sessionId, int noOfParams,
+            int appConfigParamLen, byte[] appConfigParams);
+
+    private native UwbTlvData nativeGetAppConfigurations(int sessionId, int noOfParams,
+            int appConfigParamLen, byte[] appConfigParams);
+
+    private native UwbTlvData nativeGetCapsInfo();
+
+    private native byte nativeControllerMulticastListUpdate(int sessionId, byte action,
+            byte noOfControlee, short[] address, int[]subSessionId);
+
+    private native byte nativeSetCountryCode(byte[] countryCode);
+
+    private native UwbVendorUciResponse nativeSendRawVendorCmd(int gid, int oid, byte[] payload);
+}
diff --git a/service/java/com/android/server/uwb/jni/NativeUwbRfTestManager.java b/service/java/com/android/server/uwb/jni/NativeUwbRfTestManager.java
new file mode 100644
index 0000000..7949323
--- /dev/null
+++ b/service/java/com/android/server/uwb/jni/NativeUwbRfTestManager.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+
+/* NativeUwbRfTestManager is unused now*/
+/*package com.android.server.uwb.jni;
+
+import android.util.Log;
+
+import com.android.server.uwb.test.UwbTestLoopBackTestResult;
+import com.android.server.uwb.test.UwbTestPeriodicTxResult;
+import com.android.server.uwb.test.UwbTestRxPacketErrorRateResult;
+import com.android.server.uwb.test.UwbTestRxResult;
+
+public class NativeUwbRfTestManager {
+    private static final String TAG = NativeUwbRfTestManager.class.getSimpleName();
+
+    protected INativeUwbManager.RfTestNotification mRfTestListener;
+
+    public NativeUwbRfTestManager() {
+        nativeInit();
+    }
+
+    public void setDeviceListener(INativeUwbManager.RfTestNotification rftestListener) {
+        mRfTestListener = rftestListener;
+    }
+
+    public void onPeriodicTxDataNotificationReceived(UwbTestPeriodicTxResult periodicTxTestResult) {
+        Log.d(TAG, "onPeriodicTxDataNotificationReceived : " + periodicTxTestResult);
+        mRfTestListener.onPeriodicTxDataNotificationReceived(periodicTxTestResult);
+    }
+
+    public void onPerRxDataNotificationReceived(UwbTestRxPacketErrorRateResult perRxTestResult) {
+        Log.d(TAG, "onPerRxDataNotificationReceived : " + perRxTestResult);
+        mRfTestListener.onPerRxDataNotificationReceived(perRxTestResult);
+    }
+
+    public void onLoopBackTestDataNotificationReceived(UwbTestLoopBackTestResult loopBackResult) {
+        Log.d(TAG, "onLoopBackTestDataNotificationReceived : " + loopBackResult);
+        mRfTestListener.onLoopBackTestDataNotificationReceived(loopBackResult);
+    }
+
+    public void onRxTestDataNotificationReceived(UwbTestRxResult rxTestResult) {
+        Log.d(TAG, "onRxTestDataNotificationReceived : " + rxTestResult);
+        mRfTestListener.onRxTestDataNotificationReceived(rxTestResult);
+    }
+
+    public synchronized byte[] setRfTestConfigurations(int sessionId, int noOfParams,
+            int testConfigParamLen, byte[] testConfigParams) {
+        return nativeSetTestConfigurations(
+                sessionId, noOfParams, testConfigParamLen, testConfigParams);
+    }
+
+    public synchronized byte[] getRfTestConfigurations(int sessionId, int noOfParams,
+            int testConfigParamLen, byte[] testConfigParams) {
+        return nativeGetTestConfigurations(
+                sessionId, noOfParams, testConfigParamLen, testConfigParams);
+    }
+
+    public synchronized byte startPeriodicTxTest(byte[] psduData) {
+        return nativeStartPeriodicTxTest(psduData);
+    }
+
+    public synchronized byte startPerRxTest(byte[] refPsduData) {
+        return nativeStartPerRxTest(refPsduData);
+    }
+
+    public synchronized byte startUwbLoopBackTest(byte[] psduData) {
+        return nativeStartUwbLoopBackTest(psduData);
+    }
+
+    public synchronized byte startRxTest() {
+        return nativeStartRxTest();
+    }
+
+    public synchronized byte stopRfTest() {
+        return nativeStopRfTest();
+    }
+
+    private native boolean nativeInit();
+    private native byte nativeStopRfTest();
+    private native byte nativeStartPerRxTest(byte[] refPsduData);
+    private native byte nativeStartPeriodicTxTest(byte[] psduData);
+    private native byte nativeStartUwbLoopBackTest(byte[] psduData);
+    private native byte nativeStartRxTest();
+    private native byte[] nativeSetTestConfigurations(int sessionId, int noOfParams,
+            int testConfigParamLen, byte[] testConfigParams);
+    private native byte[] nativeGetTestConfigurations(int sessionId, int noOfParams,
+            int testConfigParamLen, byte[] testConfigParams);
+}*/
diff --git a/service/java/com/android/server/uwb/multchip/UwbMultichipData.java b/service/java/com/android/server/uwb/multchip/UwbMultichipData.java
new file mode 100644
index 0000000..18013f4
--- /dev/null
+++ b/service/java/com/android/server/uwb/multchip/UwbMultichipData.java
@@ -0,0 +1,149 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.multchip;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.uwb.ChipGroupInfo;
+import com.android.uwb.ChipInfo;
+import com.android.uwb.Coordinates;
+import com.android.uwb.UwbChipConfig;
+import com.android.uwb.XmlParser;
+import com.android.uwb.resources.R;
+
+import com.google.common.base.Strings;
+import com.google.uwb.support.multichip.ChipInfoParams;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.BufferedInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.datatype.DatatypeConfigurationException;
+
+/**
+ * Manages UWB chip information (such as id and position) for a multi-chip device.
+ */
+public class UwbMultichipData {
+    private static final String TAG = "UwbMultichipData";
+    private final Context mContext;
+    private String mDefaultChipId = "default";
+    private List<ChipInfoParams> mChipInfoParamsList =
+            List.of(ChipInfoParams.createBuilder().setChipId(mDefaultChipId).build());
+
+    public UwbMultichipData(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Reads in a configuration file to initialize chip info, if the device is a multi-chip system
+     * a configuration file is defined and available.
+     *
+     * <p>If the device is single-chip, or if no configuration file is available, default values are
+     * used.
+     */
+    public void initialize() {
+        if (mContext.getResources().getBoolean(R.bool.config_isMultichip)) {
+            String filePath =
+                    mContext.getResources().getString(R.string.config_multichipConfigPath);
+            if (Strings.isNullOrEmpty(filePath)) {
+                Log.w(TAG, "Multichip is set to true, but configuration file is not defined.");
+            } else {
+                readConfigurationFile(filePath);
+            }
+        }
+    }
+
+    /**
+     * Returns a list of UWB chip infos in a {@link ChipInfoParams} object.
+     *
+     * Callers can invoke methods on a specific UWB chip by passing its {@code chipId} to the
+     * method, which can be determined by calling:
+     * <pre>
+     * {@code
+     * List<ChipInfoParams> chipInfos = getChipInfos();
+     * for (ChipInfoParams chipInfo : chipInfos) {
+     *     String chipId = chipInfo.getChipId();
+     * }
+     * }
+     * </pre>
+     *
+     * @return list of {@link ChipInfoParams} containing info about UWB chips for a multi-HAL
+     * system, or a list of info for a single chip for a single HAL system.
+     */
+    public List<ChipInfoParams> getChipInfos() {
+        return mChipInfoParamsList;
+    }
+
+    /**
+     * Returns the default UWB chip identifier.
+     *
+     * If callers do not pass a specific {@code chipId} to UWB methods, then the method will be
+     * invoked on the default chip, which is determined at system initialization from a
+     * configuration file.
+     *
+     * @return default UWB chip identifier for a multi-HAL system, or the identifier of the only UWB
+     * chip in a single HAL system.
+     */
+    public String getDefaultChipId() {
+        return mDefaultChipId;
+    }
+
+    private void readConfigurationFile(String filePath) {
+        try {
+            InputStream stream = new BufferedInputStream(new FileInputStream(filePath));
+            UwbChipConfig uwbChipConfig = XmlParser.read(stream);
+            mDefaultChipId = uwbChipConfig.getDefaultChipId();
+            // Reset mChipInfoParamsList so that it can be populated with values from configuration
+            // file.
+            mChipInfoParamsList = new ArrayList<>();
+            List<ChipGroupInfo> chipGroups = uwbChipConfig.getChipGroup();
+            for (ChipGroupInfo chipGroup : chipGroups) {
+                List<ChipInfo> chips = chipGroup.getChip();
+                for (ChipInfo chip : chips) {
+                    String chipId = chip.getId();
+                    Coordinates position = chip.getPosition();
+                    double x, y, z;
+                    if (position == null) {
+                        x = 0.0;
+                        y = 0.0;
+                        z = 0.0;
+                    } else {
+                        x = position.getX() == null ? 0.0 : position.getX().doubleValue();
+                        y = position.getY() == null ? 0.0 : position.getY().doubleValue();
+                        z = position.getZ() == null ? 0.0 : position.getZ().doubleValue();
+                    }
+                    Log.d(TAG,
+                            "Chip with id " + chipId + " has position " + x + ", " + y + ", " + z);
+                    mChipInfoParamsList
+                            .add(ChipInfoParams.createBuilder()
+                                    .setChipId(chipId)
+                                    .setPositionX(x)
+                                    .setPositionY(y)
+                                    .setPositionZ(z).build());
+                }
+            }
+        } catch (XmlPullParserException | IOException | DatatypeConfigurationException e) {
+            Log.e(TAG, "Cannot read file " + filePath, e);
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/params/CccDecoder.java b/service/java/com/android/server/uwb/params/CccDecoder.java
new file mode 100644
index 0000000..a88f71f
--- /dev/null
+++ b/service/java/com/android/server/uwb/params/CccDecoder.java
@@ -0,0 +1,177 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import static com.android.server.uwb.config.CapabilityParam.CCC_CHANNEL_5;
+import static com.android.server.uwb.config.CapabilityParam.CCC_CHANNEL_9;
+import static com.android.server.uwb.config.CapabilityParam.CCC_CHAPS_PER_SLOT_12;
+import static com.android.server.uwb.config.CapabilityParam.CCC_CHAPS_PER_SLOT_24;
+import static com.android.server.uwb.config.CapabilityParam.CCC_CHAPS_PER_SLOT_3;
+import static com.android.server.uwb.config.CapabilityParam.CCC_CHAPS_PER_SLOT_4;
+import static com.android.server.uwb.config.CapabilityParam.CCC_CHAPS_PER_SLOT_6;
+import static com.android.server.uwb.config.CapabilityParam.CCC_CHAPS_PER_SLOT_8;
+import static com.android.server.uwb.config.CapabilityParam.CCC_CHAPS_PER_SLOT_9;
+import static com.android.server.uwb.config.CapabilityParam.CCC_HOPPING_CONFIG_MODE_ADAPTIVE;
+import static com.android.server.uwb.config.CapabilityParam.CCC_HOPPING_CONFIG_MODE_CONTINUOUS;
+import static com.android.server.uwb.config.CapabilityParam.CCC_HOPPING_CONFIG_MODE_NONE;
+import static com.android.server.uwb.config.CapabilityParam.CCC_HOPPING_SEQUENCE_AES;
+import static com.android.server.uwb.config.CapabilityParam.CCC_HOPPING_SEQUENCE_DEFAULT;
+import static com.android.server.uwb.config.CapabilityParam.CCC_SUPPORTED_CHANNELS;
+import static com.android.server.uwb.config.CapabilityParam.CCC_SUPPORTED_CHAPS_PER_SLOT;
+import static com.android.server.uwb.config.CapabilityParam.CCC_SUPPORTED_HOPPING_CONFIG_MODES_AND_SEQUENCES;
+import static com.android.server.uwb.config.CapabilityParam.CCC_SUPPORTED_PULSE_SHAPE_COMBOS;
+import static com.android.server.uwb.config.CapabilityParam.CCC_SUPPORTED_RAN_MULTIPLIER;
+import static com.android.server.uwb.config.CapabilityParam.CCC_SUPPORTED_SYNC_CODES;
+import static com.android.server.uwb.config.CapabilityParam.CCC_SUPPORTED_UWB_CONFIGS;
+import static com.android.server.uwb.config.CapabilityParam.CCC_SUPPORTED_VERSIONS;
+
+import static com.google.uwb.support.ccc.CccParams.CHAPS_PER_SLOT_12;
+import static com.google.uwb.support.ccc.CccParams.CHAPS_PER_SLOT_24;
+import static com.google.uwb.support.ccc.CccParams.CHAPS_PER_SLOT_3;
+import static com.google.uwb.support.ccc.CccParams.CHAPS_PER_SLOT_4;
+import static com.google.uwb.support.ccc.CccParams.CHAPS_PER_SLOT_6;
+import static com.google.uwb.support.ccc.CccParams.CHAPS_PER_SLOT_8;
+import static com.google.uwb.support.ccc.CccParams.CHAPS_PER_SLOT_9;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_CONFIG_MODE_ADAPTIVE;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_CONFIG_MODE_CONTINUOUS;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_CONFIG_MODE_NONE;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_SEQUENCE_AES;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_SEQUENCE_DEFAULT;
+import static com.google.uwb.support.ccc.CccParams.UWB_CHANNEL_5;
+import static com.google.uwb.support.ccc.CccParams.UWB_CHANNEL_9;
+
+import com.android.server.uwb.config.ConfigParam;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.ccc.CccProtocolVersion;
+import com.google.uwb.support.ccc.CccPulseShapeCombo;
+import com.google.uwb.support.ccc.CccRangingStartedParams;
+import com.google.uwb.support.ccc.CccSpecificationParams;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * CCC decoder
+ */
+public class CccDecoder extends TlvDecoder {
+    @Override
+    public <T extends Params> T getParams(TlvDecoderBuffer tlvs, Class<T> paramsType)
+            throws IllegalArgumentException {
+        if (CccRangingStartedParams.class.equals(paramsType)) {
+            return (T) getCccRangingStartedParamsFromTlvBuffer(tlvs);
+        }
+        if (CccSpecificationParams.class.equals(paramsType)) {
+            return (T) getCccSpecificationParamsFromTlvBuffer(tlvs);
+        }
+        return null;
+    }
+
+    private static boolean isBitSet(int flags, int mask) {
+        return (flags & mask) != 0;
+    }
+
+    private CccRangingStartedParams getCccRangingStartedParamsFromTlvBuffer(TlvDecoderBuffer tlvs) {
+        byte[] hopModeKey = tlvs.getByteArray(ConfigParam.HOP_MODE_KEY);
+        int hopModeKeyInt = ByteBuffer.wrap(hopModeKey).order(ByteOrder.LITTLE_ENDIAN).getInt();
+        return new CccRangingStartedParams.Builder()
+                // STS_Index0  0 - 0x3FFFFFFFF
+                .setStartingStsIndex(tlvs.getInt(ConfigParam.STS_INDEX))
+                .setHopModeKey(hopModeKeyInt)
+                //  UWB_Time0 0 - 0xFFFFFFFFFFFFFFFF  UWB_INITIATION_TIME
+                .setUwbTime0(tlvs.getLong(ConfigParam.UWB_TIME0))
+                // RANGING_INTERVAL = RAN_Multiplier * 96
+                .setRanMultiplier(tlvs.getInt(ConfigParam.RANGING_INTERVAL) / 96)
+                .setSyncCodeIndex(tlvs.getByte(ConfigParam.PREAMBLE_CODE_INDEX))
+                .build();
+    }
+
+    private CccSpecificationParams getCccSpecificationParamsFromTlvBuffer(TlvDecoderBuffer tlvs) {
+        CccSpecificationParams.Builder builder = new CccSpecificationParams.Builder();
+        byte[] versions = tlvs.getByteArray(CCC_SUPPORTED_VERSIONS);
+        if (versions.length % 2 != 0) {
+            throw new IllegalArgumentException("Invalid supported protocol versions len "
+                    + versions.length);
+        }
+        for (int i = 0; i < versions.length; i += 2) {
+            builder.addProtocolVersion(CccProtocolVersion.fromBytes(versions, i));
+        }
+        byte[] configs = tlvs.getByteArray(CCC_SUPPORTED_UWB_CONFIGS);
+        for (int i = 0; i < configs.length; i++) {
+            builder.addUwbConfig(configs[i]);
+        }
+        byte[] pulse_shape_combos = tlvs.getByteArray(CCC_SUPPORTED_PULSE_SHAPE_COMBOS);
+        for (int i = 0; i < pulse_shape_combos.length; i++) {
+            builder.addPulseShapeCombo(CccPulseShapeCombo.fromBytes(pulse_shape_combos, i));
+        }
+        builder.setRanMultiplier(tlvs.getInt(CCC_SUPPORTED_RAN_MULTIPLIER));
+        byte chapsPerslot = tlvs.getByte(CCC_SUPPORTED_CHAPS_PER_SLOT);
+        if (isBitSet(chapsPerslot, CCC_CHAPS_PER_SLOT_3)) {
+            builder.addChapsPerSlot(CHAPS_PER_SLOT_3);
+        }
+        if (isBitSet(chapsPerslot, CCC_CHAPS_PER_SLOT_4)) {
+            builder.addChapsPerSlot(CHAPS_PER_SLOT_4);
+        }
+        if (isBitSet(chapsPerslot, CCC_CHAPS_PER_SLOT_6)) {
+            builder.addChapsPerSlot(CHAPS_PER_SLOT_6);
+        }
+        if (isBitSet(chapsPerslot, CCC_CHAPS_PER_SLOT_8)) {
+            builder.addChapsPerSlot(CHAPS_PER_SLOT_8);
+        }
+        if (isBitSet(chapsPerslot, CCC_CHAPS_PER_SLOT_9)) {
+            builder.addChapsPerSlot(CHAPS_PER_SLOT_9);
+        }
+        if (isBitSet(chapsPerslot, CCC_CHAPS_PER_SLOT_12)) {
+            builder.addChapsPerSlot(CHAPS_PER_SLOT_12);
+        }
+        if (isBitSet(chapsPerslot, CCC_CHAPS_PER_SLOT_24)) {
+            builder.addChapsPerSlot(CHAPS_PER_SLOT_24);
+        }
+        // Don't use TlvDecodeBuffer#getInt() to avoid conversion to little endian.
+        int syncCodes = ByteBuffer.wrap(tlvs.getByteArray(CCC_SUPPORTED_SYNC_CODES)).getInt();
+        for (int i = 0; i < 32; i++) {
+            if (isBitSet(syncCodes, 1 << i)) {
+                builder.addSyncCode(i + 1);
+            }
+        }
+        byte channels = tlvs.getByte(CCC_SUPPORTED_CHANNELS);
+        if (isBitSet(channels, CCC_CHANNEL_5)) {
+            builder.addChannel(UWB_CHANNEL_5);
+        }
+        if (isBitSet(channels, CCC_CHANNEL_9)) {
+            builder.addChannel(UWB_CHANNEL_9);
+        }
+        byte hoppingConfigModesAndSequences =
+                tlvs.getByte(CCC_SUPPORTED_HOPPING_CONFIG_MODES_AND_SEQUENCES);
+        if (isBitSet(hoppingConfigModesAndSequences, CCC_HOPPING_CONFIG_MODE_NONE)) {
+            builder.addHoppingConfigMode(HOPPING_CONFIG_MODE_NONE);
+        }
+        if (isBitSet(hoppingConfigModesAndSequences, CCC_HOPPING_CONFIG_MODE_CONTINUOUS)) {
+            builder.addHoppingConfigMode(HOPPING_CONFIG_MODE_CONTINUOUS);
+        }
+        if (isBitSet(hoppingConfigModesAndSequences, CCC_HOPPING_CONFIG_MODE_ADAPTIVE)) {
+            builder.addHoppingConfigMode(HOPPING_CONFIG_MODE_ADAPTIVE);
+        }
+        if (isBitSet(hoppingConfigModesAndSequences, CCC_HOPPING_SEQUENCE_AES)) {
+            builder.addHoppingSequence(HOPPING_SEQUENCE_AES);
+        }
+        if (isBitSet(hoppingConfigModesAndSequences, CCC_HOPPING_SEQUENCE_DEFAULT)) {
+            builder.addHoppingSequence(HOPPING_SEQUENCE_DEFAULT);
+        }
+        return builder.build();
+    }
+}
diff --git a/service/java/com/android/server/uwb/params/CccEncoder.java b/service/java/com/android/server/uwb/params/CccEncoder.java
new file mode 100644
index 0000000..93a2f25
--- /dev/null
+++ b/service/java/com/android/server/uwb/params/CccEncoder.java
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import com.android.server.uwb.config.ConfigParam;
+import com.android.server.uwb.data.UwbCccConstants;
+import com.android.server.uwb.data.UwbUciConstants;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.ccc.CccOpenRangingParams;
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.fira.FiraParams;
+
+public class CccEncoder extends TlvEncoder {
+    @Override
+    public TlvBuffer getTlvBuffer(Params param) {
+        if (param instanceof CccOpenRangingParams) {
+            return getTlvBufferFromCccOpenRangingParams(param);
+        }
+        return null;
+    }
+
+    private TlvBuffer getTlvBufferFromCccOpenRangingParams(Params baseParam) {
+        CccOpenRangingParams params = (CccOpenRangingParams) baseParam;
+        int hoppingConfig = params.getHoppingConfigMode();
+        int hoppingSequence = params.getHoppingSequence();
+
+        int hoppingMode = CccParams.HOPPING_CONFIG_MODE_NONE;
+
+        switch (hoppingConfig) {
+
+            case CccParams.HOPPING_CONFIG_MODE_CONTINUOUS:
+                if (hoppingSequence == CccParams.HOPPING_SEQUENCE_DEFAULT) {
+                    hoppingMode = UwbCccConstants.HOPPING_CONFIG_MODE_CONTINUOUS_DEFAULT;
+                } else {
+                    hoppingMode = UwbCccConstants.HOPPING_CONFIG_MODE_CONTINUOUS_AES;
+                }
+                break;
+            case CccParams.HOPPING_CONFIG_MODE_ADAPTIVE:
+                if (hoppingSequence == CccParams.HOPPING_SEQUENCE_DEFAULT) {
+                    hoppingMode = UwbCccConstants.HOPPING_CONFIG_MODE_MODE_ADAPTIVE_DEFAULT;
+                } else {
+                    hoppingMode = UwbCccConstants.HOPPING_CONFIG_MODE_MODE_ADAPTIVE_AES;
+                }
+                break;
+        }
+
+        TlvBuffer tlvBuffer = new TlvBuffer.Builder()
+                .putByte(ConfigParam.DEVICE_TYPE,
+                        (byte) UwbUciConstants.DEVICE_TYPE_CONTROLEE) // DEVICE_TYPE
+                .putByte(ConfigParam.STS_CONFIG,
+                        (byte) UwbUciConstants.STS_MODE_DYNAMIC) // STS_CONFIG
+                .putByte(ConfigParam.CHANNEL_NUMBER, (byte) params.getChannel()) // CHANNEL_ID
+                .putByte(ConfigParam.NUMBER_OF_CONTROLEES,
+                        (byte) params.getNumResponderNodes()) // NUMBER_OF_ANCHORS
+                .putInt(ConfigParam.RANGING_INTERVAL,
+                        params.getRanMultiplier() * 96) //RANGING_INTERVAL = RAN_Multiplier * 96
+                .putByte(ConfigParam.RANGE_DATA_NTF_CONFIG,
+                        (byte) UwbUciConstants.RANGE_DATA_NTF_CONFIG_DISABLE) // RNG_DATA_NTF
+                .putByte(ConfigParam.DEVICE_ROLE,
+                        (byte) UwbUciConstants.RANGING_DEVICE_ROLE_INITIATOR) // DEVICE_ROLE
+                .putByte(ConfigParam.MULTI_NODE_MODE,
+                        (byte) FiraParams.MULTI_NODE_MODE_ONE_TO_MANY) // MULTI_NODE_MODE
+                .putByte(ConfigParam.SLOTS_PER_RR,
+                        (byte) params.getNumSlotsPerRound()) // SLOTS_PER_RR
+                .putByte(ConfigParam.KEY_ROTATION, (byte) 0X01) // KEY_ROTATION
+                .putByte(ConfigParam.HOPPING_MODE, (byte) hoppingMode) // HOPPING_MODE
+                .putByteArray(ConfigParam.RANGING_PROTOCOL_VER,
+                        ConfigParam.RANGING_PROTOCOL_VER_BYTE_COUNT,
+                        params.getProtocolVersion().toBytes()) // RANGING_PROTOCOL_VER
+                .putShort(ConfigParam.UWB_CONFIG_ID, (short) params.getUwbConfig()) // UWB_CONFIG_ID
+                .putByte(ConfigParam.PULSESHAPE_COMBO,
+                        params.getPulseShapeCombo().toBytes()[0]) // PULSESHAPE_COMBO
+                .putShort(ConfigParam.URSK_TTL, (short) 0x2D0) // URSK_TTL
+                // T(Slotk) =  N(Chap_per_Slot) * T(Chap)
+                // T(Chap) = 400RSTU
+                // reference : digital key release 3 20.2 MAC Time Grid
+                .putShort(ConfigParam.SLOT_DURATION,
+                        (short) (params.getNumChapsPerSlot() * 400)) // SLOT_DURATION
+                .putByte(ConfigParam.PREAMBLE_CODE_INDEX,
+                        (byte) params.getSyncCodeIndex()) // PREAMBLE_CODE_INDEX
+                .build();
+
+        return tlvBuffer;
+    }
+}
diff --git a/service/java/com/android/server/uwb/params/FiraDecoder.java b/service/java/com/android/server/uwb/params/FiraDecoder.java
new file mode 100644
index 0000000..52605b9
--- /dev/null
+++ b/service/java/com/android/server/uwb/params/FiraDecoder.java
@@ -0,0 +1,291 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import static com.android.server.uwb.config.CapabilityParam.AOA_AZIMUTH_180;
+import static com.android.server.uwb.config.CapabilityParam.AOA_AZIMUTH_90;
+import static com.android.server.uwb.config.CapabilityParam.AOA_ELEVATION;
+import static com.android.server.uwb.config.CapabilityParam.AOA_FOM;
+import static com.android.server.uwb.config.CapabilityParam.AOA_RESULT_REQ_INTERLEAVING;
+import static com.android.server.uwb.config.CapabilityParam.BLOCK_STRIDING;
+import static com.android.server.uwb.config.CapabilityParam.CC_CONSTRAINT_LENGTH_K3;
+import static com.android.server.uwb.config.CapabilityParam.CC_CONSTRAINT_LENGTH_K7;
+import static com.android.server.uwb.config.CapabilityParam.CHANNEL_10;
+import static com.android.server.uwb.config.CapabilityParam.CHANNEL_12;
+import static com.android.server.uwb.config.CapabilityParam.CHANNEL_13;
+import static com.android.server.uwb.config.CapabilityParam.CHANNEL_14;
+import static com.android.server.uwb.config.CapabilityParam.CHANNEL_5;
+import static com.android.server.uwb.config.CapabilityParam.CHANNEL_6;
+import static com.android.server.uwb.config.CapabilityParam.CHANNEL_8;
+import static com.android.server.uwb.config.CapabilityParam.CHANNEL_9;
+import static com.android.server.uwb.config.CapabilityParam.DS_TWR_DEFERRED;
+import static com.android.server.uwb.config.CapabilityParam.DS_TWR_NON_DEFERRED;
+import static com.android.server.uwb.config.CapabilityParam.DYNAMIC_STS;
+import static com.android.server.uwb.config.CapabilityParam.DYNAMIC_STS_RESPONDER_SPECIFIC_SUBSESSION_KEY;
+import static com.android.server.uwb.config.CapabilityParam.INITIATOR;
+import static com.android.server.uwb.config.CapabilityParam.MANY_TO_MANY;
+import static com.android.server.uwb.config.CapabilityParam.ONE_TO_MANY;
+import static com.android.server.uwb.config.CapabilityParam.RESPONDER;
+import static com.android.server.uwb.config.CapabilityParam.SP0;
+import static com.android.server.uwb.config.CapabilityParam.SP1;
+import static com.android.server.uwb.config.CapabilityParam.SP3;
+import static com.android.server.uwb.config.CapabilityParam.SS_TWR_DEFERRED;
+import static com.android.server.uwb.config.CapabilityParam.SS_TWR_NON_DEFERRED;
+import static com.android.server.uwb.config.CapabilityParam.STATIC_STS;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_AOA;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_AOA_RESULT_REQ_INTERLEAVING;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_BLOCK_STRIDING;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_BPRF_PARAMETER_SETS;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_CC_CONSTRAINT_LENGTH;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_CHANNELS;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_DEVICE_ROLES;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_EXTENDED_MAC_ADDRESS;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_FIRA_MAC_VERSION_RANGE;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_FIRA_PHY_VERSION_RANGE;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_HPRF_PARAMETER_SETS;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_MULTI_NODE_MODES;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_RANGING_METHOD;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_RFRAME_CONFIG;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_STS_CONFIG;
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_UWB_INITIATION_TIME;
+import static com.android.server.uwb.config.CapabilityParam.UNICAST;
+import static com.android.server.uwb.config.CapabilityParam.UWB_INITIATION_TIME;
+
+import com.google.uwb.support.base.FlagEnum;
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraParams.BprfParameterSetCapabilityFlag;
+import com.google.uwb.support.fira.FiraParams.DeviceRoleCapabilityFlag;
+import com.google.uwb.support.fira.FiraParams.HprfParameterSetCapabilityFlag;
+import com.google.uwb.support.fira.FiraParams.MultiNodeCapabilityFlag;
+import com.google.uwb.support.fira.FiraParams.PsduDataRateCapabilityFlag;
+import com.google.uwb.support.fira.FiraParams.RangingRoundCapabilityFlag;
+import com.google.uwb.support.fira.FiraParams.RframeCapabilityFlag;
+import com.google.uwb.support.fira.FiraParams.StsCapabilityFlag;
+import com.google.uwb.support.fira.FiraProtocolVersion;
+import com.google.uwb.support.fira.FiraSpecificationParams;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.stream.IntStream;
+
+public class FiraDecoder extends TlvDecoder {
+    @Override
+    public <T extends Params> T getParams(TlvDecoderBuffer tlvs, Class<T> paramType) {
+        if (FiraSpecificationParams.class.equals(paramType)) {
+            return (T) getFiraSpecificationParamsFromTlvBuffer(tlvs);
+        }
+        return null;
+    }
+
+    private static boolean isBitSet(int flags, int mask) {
+        return (flags & mask) != 0;
+    }
+
+    private FiraSpecificationParams getFiraSpecificationParamsFromTlvBuffer(TlvDecoderBuffer tlvs) {
+        FiraSpecificationParams.Builder builder = new FiraSpecificationParams.Builder();
+        byte[] phyVersions = tlvs.getByteArray(SUPPORTED_FIRA_PHY_VERSION_RANGE);
+        builder.setMinPhyVersionSupported(FiraProtocolVersion.fromBytes(phyVersions, 0));
+        builder.setMaxPhyVersionSupported(FiraProtocolVersion.fromBytes(phyVersions, 2));
+        byte[] macVersions = tlvs.getByteArray(SUPPORTED_FIRA_MAC_VERSION_RANGE);
+        builder.setMinMacVersionSupported(FiraProtocolVersion.fromBytes(macVersions, 0));
+        builder.setMaxMacVersionSupported(FiraProtocolVersion.fromBytes(macVersions, 2));
+
+        byte deviceRolesUci = tlvs.getByte(SUPPORTED_DEVICE_ROLES);
+        EnumSet<DeviceRoleCapabilityFlag> deviceRoles =
+                EnumSet.noneOf(DeviceRoleCapabilityFlag.class);
+        if (isBitSet(deviceRolesUci, INITIATOR)) {
+            // This assumes both controller + controlee is supported.
+            deviceRoles.add(DeviceRoleCapabilityFlag.HAS_CONTROLLER_INITIATOR_SUPPORT);
+            deviceRoles.add(DeviceRoleCapabilityFlag.HAS_CONTROLEE_INITIATOR_SUPPORT);
+        }
+        if (isBitSet(deviceRolesUci, RESPONDER)) {
+            // This assumes both controller + controlee is supported.
+            deviceRoles.add(DeviceRoleCapabilityFlag.HAS_CONTROLLER_RESPONDER_SUPPORT);
+            deviceRoles.add(DeviceRoleCapabilityFlag.HAS_CONTROLEE_RESPONDER_SUPPORT);
+        }
+        builder.setDeviceRoleCapabilities(deviceRoles);
+
+        byte rangingMethodUci = tlvs.getByte(SUPPORTED_RANGING_METHOD);
+        EnumSet<RangingRoundCapabilityFlag> rangingRoundFlag = EnumSet.noneOf(
+                RangingRoundCapabilityFlag.class);
+        if (isBitSet(rangingMethodUci, DS_TWR_DEFERRED)) {
+            rangingRoundFlag.add(RangingRoundCapabilityFlag.HAS_DS_TWR_SUPPORT);
+        }
+        if (isBitSet(rangingMethodUci, SS_TWR_DEFERRED)) {
+            rangingRoundFlag.add(RangingRoundCapabilityFlag.HAS_SS_TWR_SUPPORT);
+        }
+        builder.setRangingRoundCapabilities(rangingRoundFlag);
+
+        // TODO(b/209053358): This does not align with UCI spec.
+        if (isBitSet(rangingMethodUci, DS_TWR_NON_DEFERRED)
+                || isBitSet(rangingMethodUci, SS_TWR_NON_DEFERRED)) {
+            builder.hasNonDeferredModeSupport(true);
+        }
+
+        byte stsConfigUci = tlvs.getByte(SUPPORTED_STS_CONFIG);
+        EnumSet<StsCapabilityFlag> stsCapabilityFlag = EnumSet.noneOf(StsCapabilityFlag.class);
+        if (isBitSet(stsConfigUci, STATIC_STS)) {
+            stsCapabilityFlag.add(StsCapabilityFlag.HAS_STATIC_STS_SUPPORT);
+        }
+        if (isBitSet(stsConfigUci, DYNAMIC_STS)) {
+            stsCapabilityFlag.add(StsCapabilityFlag.HAS_DYNAMIC_STS_SUPPORT);
+        }
+        if (isBitSet(stsConfigUci, DYNAMIC_STS_RESPONDER_SPECIFIC_SUBSESSION_KEY)) {
+            stsCapabilityFlag.add(
+                    StsCapabilityFlag.HAS_DYNAMIC_STS_INDIVIDUAL_CONTROLEE_KEY_SUPPORT);
+        }
+        builder.setStsCapabilities(stsCapabilityFlag);
+
+        byte multiNodeUci = tlvs.getByte(SUPPORTED_MULTI_NODE_MODES);
+        EnumSet<MultiNodeCapabilityFlag> multiNodeFlag =
+                EnumSet.noneOf(MultiNodeCapabilityFlag.class);
+        if (isBitSet(multiNodeUci, UNICAST)) {
+            multiNodeFlag.add(MultiNodeCapabilityFlag.HAS_UNICAST_SUPPORT);
+        }
+        if (isBitSet(multiNodeUci, ONE_TO_MANY)) {
+            multiNodeFlag.add(MultiNodeCapabilityFlag.HAS_ONE_TO_MANY_SUPPORT);
+        }
+        if (isBitSet(multiNodeUci, MANY_TO_MANY)) {
+            multiNodeFlag.add(MultiNodeCapabilityFlag.HAS_MANY_TO_MANY_SUPPORT);
+        }
+        builder.setMultiNodeCapabilities(multiNodeFlag);
+
+        byte blockStridingUci = tlvs.getByte(SUPPORTED_BLOCK_STRIDING);
+        if (isBitSet(blockStridingUci, BLOCK_STRIDING)) {
+            builder.hasBlockStridingSupport(true);
+        }
+
+        byte initiationTimeUci = tlvs.getByte(SUPPORTED_UWB_INITIATION_TIME);
+        if (isBitSet(initiationTimeUci, UWB_INITIATION_TIME)) {
+            builder.hasInitiationTimeSupport(true);
+        }
+
+        byte channelsUci = tlvs.getByte(SUPPORTED_CHANNELS);
+        List<Integer> channels = new ArrayList<>();
+        if (isBitSet(channelsUci, CHANNEL_5)) {
+            channels.add(5);
+        }
+        if (isBitSet(channelsUci, CHANNEL_6)) {
+            channels.add(6);
+        }
+        if (isBitSet(channelsUci, CHANNEL_8)) {
+            channels.add(8);
+        }
+        if (isBitSet(channelsUci, CHANNEL_9)) {
+            channels.add(9);
+        }
+        if (isBitSet(channelsUci, CHANNEL_10)) {
+            channels.add(10);
+        }
+        if (isBitSet(channelsUci, CHANNEL_12)) {
+            channels.add(12);
+        }
+        if (isBitSet(channelsUci, CHANNEL_13)) {
+            channels.add(13);
+        }
+        if (isBitSet(channelsUci, CHANNEL_14)) {
+            channels.add(14);
+        }
+        builder.setSupportedChannels(channels);
+
+        byte rframeConfigUci = tlvs.getByte(SUPPORTED_RFRAME_CONFIG);
+        EnumSet<RframeCapabilityFlag> rframeConfigFlag =
+                EnumSet.noneOf(RframeCapabilityFlag.class);
+        if (isBitSet(rframeConfigUci, SP0)) {
+            rframeConfigFlag.add(RframeCapabilityFlag.HAS_SP0_RFRAME_SUPPORT);
+        }
+        if (isBitSet(rframeConfigUci, SP1)) {
+            rframeConfigFlag.add(RframeCapabilityFlag.HAS_SP1_RFRAME_SUPPORT);
+        }
+        if (isBitSet(rframeConfigUci, SP3)) {
+            rframeConfigFlag.add(RframeCapabilityFlag.HAS_SP3_RFRAME_SUPPORT);
+        }
+        builder.setRframeCapabilities(rframeConfigFlag);
+
+        byte bprfSets = tlvs.getByte(SUPPORTED_BPRF_PARAMETER_SETS);
+        int bprfSetsValue = Integer.valueOf(bprfSets);
+        EnumSet<BprfParameterSetCapabilityFlag> bprfFlag;
+        bprfFlag = FlagEnum.toEnumSet(bprfSetsValue, BprfParameterSetCapabilityFlag.values());
+        builder.setBprfParameterSetCapabilities(bprfFlag);
+
+        byte[] hprfSets = tlvs.getByteArray(SUPPORTED_HPRF_PARAMETER_SETS);
+        // Extend the 5 bytes from HAL to 8 bytes for long.
+        long hprfSetsValue = new BigInteger(hprfSets).longValue();
+        EnumSet<HprfParameterSetCapabilityFlag> hprfFlag;
+        hprfFlag = FlagEnum.longToEnumSet(
+                hprfSetsValue, HprfParameterSetCapabilityFlag.values());
+        builder.setHprfParameterSetCapabilities(hprfFlag);
+
+        EnumSet<FiraParams.PrfCapabilityFlag> prfFlag =
+                EnumSet.noneOf(FiraParams.PrfCapabilityFlag.class);
+        boolean hasBprfSupport = bprfSets != 0;
+        if (hasBprfSupport) {
+            prfFlag.add(FiraParams.PrfCapabilityFlag.HAS_BPRF_SUPPORT);
+        }
+        boolean hasHprfSupport =
+                IntStream.range(0, hprfSets.length).parallel().anyMatch(i -> hprfSets[i] != 0);
+        if (hasHprfSupport) {
+            prfFlag.add(FiraParams.PrfCapabilityFlag.HAS_HPRF_SUPPORT);
+        }
+        builder.setPrfCapabilities(prfFlag);
+
+        byte ccConstraintUci = tlvs.getByte(SUPPORTED_CC_CONSTRAINT_LENGTH);
+        EnumSet<PsduDataRateCapabilityFlag> psduRateFlag =
+                EnumSet.noneOf(PsduDataRateCapabilityFlag.class);
+        if (isBitSet(ccConstraintUci, CC_CONSTRAINT_LENGTH_K3) && hasBprfSupport) {
+            psduRateFlag.add(PsduDataRateCapabilityFlag.HAS_6M81_SUPPORT);
+        }
+        if (isBitSet(ccConstraintUci, CC_CONSTRAINT_LENGTH_K7) && hasBprfSupport) {
+            psduRateFlag.add(PsduDataRateCapabilityFlag.HAS_7M80_SUPPORT);
+        }
+        if (isBitSet(ccConstraintUci, CC_CONSTRAINT_LENGTH_K3) && hasHprfSupport) {
+            psduRateFlag.add(PsduDataRateCapabilityFlag.HAS_27M2_SUPPORT);
+        }
+        if (isBitSet(ccConstraintUci, CC_CONSTRAINT_LENGTH_K7) && hasHprfSupport) {
+            psduRateFlag.add(PsduDataRateCapabilityFlag.HAS_31M2_SUPPORT);
+        }
+        builder.setPsduDataRateCapabilities(psduRateFlag);
+
+        byte aoaUci = tlvs.getByte(SUPPORTED_AOA);
+        EnumSet<FiraParams.AoaCapabilityFlag> aoaFlag =
+                EnumSet.noneOf(FiraParams.AoaCapabilityFlag.class);
+        if (isBitSet(aoaUci, AOA_AZIMUTH_90)) {
+            aoaFlag.add(FiraParams.AoaCapabilityFlag.HAS_AZIMUTH_SUPPORT);
+        }
+        if (isBitSet(aoaUci, AOA_AZIMUTH_180)) {
+            aoaFlag.add(FiraParams.AoaCapabilityFlag.HAS_FULL_AZIMUTH_SUPPORT);
+        }
+        if (isBitSet(aoaUci, AOA_ELEVATION)) {
+            aoaFlag.add(FiraParams.AoaCapabilityFlag.HAS_ELEVATION_SUPPORT);
+        }
+        if (isBitSet(aoaUci, AOA_FOM)) {
+            aoaFlag.add(FiraParams.AoaCapabilityFlag.HAS_FOM_SUPPORT);
+        }
+        byte aoaInterleavingUci = tlvs.getByte(SUPPORTED_AOA_RESULT_REQ_INTERLEAVING);
+        if (isBitSet(aoaInterleavingUci, AOA_RESULT_REQ_INTERLEAVING)) {
+            aoaFlag.add(FiraParams.AoaCapabilityFlag.HAS_INTERLEAVING_SUPPORT);
+        }
+        builder.setAoaCapabilities(aoaFlag);
+
+        // TODO(b/209053358): This is not present in the FiraSpecificationParams.
+        byte extendedMacUci = tlvs.getByte(SUPPORTED_EXTENDED_MAC_ADDRESS);
+        return builder.build();
+    }
+}
diff --git a/service/java/com/android/server/uwb/params/FiraEncoder.java b/service/java/com/android/server/uwb/params/FiraEncoder.java
new file mode 100644
index 0000000..fc182c7
--- /dev/null
+++ b/service/java/com/android/server/uwb/params/FiraEncoder.java
@@ -0,0 +1,173 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import android.uwb.UwbAddress;
+
+import com.android.server.uwb.config.ConfigParam;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.fira.FiraOpenSessionParams;
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraRangingReconfigureParams;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+public class FiraEncoder extends TlvEncoder {
+    @Override
+    public TlvBuffer getTlvBuffer(Params param) {
+        if (param instanceof FiraOpenSessionParams) {
+            return getTlvBufferFromFiraOpenSessionParams(param);
+        }
+
+        if (param instanceof FiraRangingReconfigureParams) {
+            return getTlvBufferFromFiraRangingReconfigureParams(param);
+        }
+        return null;
+    }
+
+    private TlvBuffer getTlvBufferFromFiraOpenSessionParams(Params baseParam) {
+        FiraOpenSessionParams params = (FiraOpenSessionParams) baseParam;
+        ByteBuffer dstAddressList = ByteBuffer.allocate(1024);
+        for (UwbAddress address : params.getDestAddressList()) {
+            dstAddressList.put(TlvUtil.getReverseBytes(address.toBytes()));
+        }
+
+        int resultReportConfig = getResultReportConfig(params);
+        int rangingRoundControl = getRangingRoundControl(params);
+
+        TlvBuffer tlvBuffer = new TlvBuffer.Builder()
+                .putByte(ConfigParam.DEVICE_TYPE, (byte) params.getDeviceType())
+                .putByte(ConfigParam.RANGING_ROUND_USAGE, (byte) params.getRangingRoundUsage())
+                .putByte(ConfigParam.STS_CONFIG, (byte) params.getStsConfig())
+                .putByte(ConfigParam.MULTI_NODE_MODE, (byte) params.getMultiNodeMode())
+                .putByte(ConfigParam.CHANNEL_NUMBER, (byte) params.getChannelNumber())
+                .putByte(ConfigParam.NUMBER_OF_CONTROLEES,
+                        (byte) params.getDestAddressList().size())
+                .putByteArray(ConfigParam.DEVICE_MAC_ADDRESS, params.getDeviceAddress().size(),
+                        TlvUtil.getReverseBytes(params.getDeviceAddress().toBytes()))
+                .putByteArray(ConfigParam.DST_MAC_ADDRESS, dstAddressList.position(),
+                        Arrays.copyOf(dstAddressList.array(), dstAddressList.position()))
+                .putShort(ConfigParam.SLOT_DURATION, (short) params.getSlotDurationRstu())
+                .putInt(ConfigParam.RANGING_INTERVAL, params.getRangingIntervalMs())
+                .putByte(ConfigParam.MAC_FCS_TYPE, (byte) params.getFcsType())
+                .putByte(ConfigParam.RANGING_ROUND_CONTROL,
+                        (byte) rangingRoundControl/* params.getMeasurementReportType()*/)
+                .putByte(ConfigParam.AOA_RESULT_REQ, (byte) params.getAoaResultRequest())
+                .putByte(ConfigParam.RANGE_DATA_NTF_CONFIG, (byte) params.getRangeDataNtfConfig())
+                .putShort(ConfigParam.RANGE_DATA_NTF_PROXIMITY_NEAR,
+                        (short) params.getRangeDataNtfProximityNear())
+                .putShort(ConfigParam.RANGE_DATA_NTF_PROXIMITY_FAR,
+                        (short) params.getRangeDataNtfProximityFar())
+                .putByte(ConfigParam.DEVICE_ROLE, (byte) params.getDeviceRole())
+                .putByte(ConfigParam.RFRAME_CONFIG, (byte) params.getRframeConfig())
+                .putByte(ConfigParam.PREAMBLE_CODE_INDEX, (byte) params.getPreambleCodeIndex())
+                .putByte(ConfigParam.SFD_ID, (byte) params.getSfdId())
+                .putByte(ConfigParam.PSDU_DATA_RATE, (byte) params.getPsduDataRate())
+                .putByte(ConfigParam.PREAMBLE_DURATION, (byte) params.getPreambleDuration())
+                .putByte(ConfigParam.SLOTS_PER_RR, (byte) params.getSlotsPerRangingRound())
+                .putByte(ConfigParam.TX_ADAPTIVE_PAYLOAD_POWER,
+                        params.isTxAdaptivePayloadPowerEnabled() ? (byte) 1 : (byte) 0)
+                .putByte(ConfigParam.PRF_MODE, (byte) params.getPrfMode())
+                .putByte(ConfigParam.KEY_ROTATION,
+                        params.isKeyRotationEnabled() ? (byte) 1 : (byte) 0)
+                .putByte(ConfigParam.KEY_ROTATION_RATE, (byte) params.getKeyRotationRate())
+                .putByte(ConfigParam.SESSION_PRIORITY, (byte) params.getSessionPriority())
+                .putByte(ConfigParam.MAC_ADDRESS_MODE, (byte) params.getMacAddressMode())
+                .putByteArray(ConfigParam.VENDOR_ID,
+                        TlvUtil.getReverseBytes(params.getVendorId()))
+                .putByteArray(ConfigParam.STATIC_STS_IV,
+                        params.getStaticStsIV())
+                .putByte(ConfigParam.NUMBER_OF_STS_SEGMENTS, (byte) params.getStsSegmentCount())
+                .putShort(ConfigParam.MAX_RR_RETRY, (short) params.getMaxRangingRoundRetries())
+                .putInt(ConfigParam.UWB_INITIATION_TIME, params.getInitiationTimeMs())
+                .putByte(ConfigParam.HOPPING_MODE,
+                        (byte) params.getHoppingMode())
+                .putByte(ConfigParam.BLOCK_STRIDE_LENGTH, (byte) params.getBlockStrideLength())
+                .putByte(ConfigParam.RESULT_REPORT_CONFIG, (byte) resultReportConfig)
+                .putByte(ConfigParam.IN_BAND_TERMINATION_ATTEMPT_COUNT,
+                        (byte) params.getInBandTerminationAttemptCount())
+                .putInt(ConfigParam.SUB_SESSION_ID, params.getSubSessionId())
+                .putByte(ConfigParam.BPRF_PHR_DATA_RATE, (byte) params.getBprfPhrDataRate())
+                .putByte(ConfigParam.STS_LENGTH, (byte) params.getStsLength())
+                .putByte(ConfigParam.NUM_RANGE_MEASUREMENTS,
+                        (byte) params.getNumOfMsrmtFocusOnRange())
+                .putByte(ConfigParam.NUM_AOA_AZIMUTH_MEASUREMENTS,
+                        (byte) params.getNumOfMsrmtFocusOnAoaAzimuth())
+                .putByte(ConfigParam.NUM_AOA_ELEVATION_MEASUREMENTS,
+                        (byte) params.getNumOfMsrmtFocusOnAoaElevation())
+                .build();
+        return tlvBuffer;
+    }
+
+    private TlvBuffer getTlvBufferFromFiraRangingReconfigureParams(Params baseParam) {
+        FiraRangingReconfigureParams params = (FiraRangingReconfigureParams) baseParam;
+        TlvBuffer.Builder tlvBuilder = new TlvBuffer.Builder();
+        Integer blockStrideLength = params.getBlockStrideLength();
+        Integer rangeDataNtfConfig = params.getRangeDataNtfConfig();
+        Integer rangeDataProximityNear = params.getRangeDataProximityNear();
+        Integer rangeDataProximityFar = params.getRangeDataProximityFar();
+
+        if (blockStrideLength != null) {
+            tlvBuilder.putByte(ConfigParam.BLOCK_STRIDE_LENGTH,
+                    (byte) blockStrideLength.intValue());
+        }
+
+        if (rangeDataNtfConfig != null) {
+            tlvBuilder.putByte(ConfigParam.RANGE_DATA_NTF_CONFIG,
+                    (byte) rangeDataNtfConfig.intValue());
+        }
+
+        if (rangeDataProximityNear != null) {
+            tlvBuilder.putShort(ConfigParam.RANGE_DATA_NTF_PROXIMITY_NEAR,
+                    (short) rangeDataProximityNear.intValue());
+        }
+
+        if (rangeDataProximityFar != null) {
+            tlvBuilder.putShort(ConfigParam.RANGE_DATA_NTF_PROXIMITY_FAR,
+                    (short) rangeDataProximityFar.intValue());
+        }
+
+        return tlvBuilder.build();
+    }
+
+    // Merged data from other parameter values
+    private int getResultReportConfig(FiraOpenSessionParams params) {
+        int resultReportConfig = 0x00;
+        resultReportConfig |= params.hasTimeOfFlightReport() ? 0x01 : 0x00;
+        resultReportConfig |= params.hasAngleOfArrivalAzimuthReport() ? 0x02 : 0x00;
+        resultReportConfig |= params.hasAngleOfArrivalElevationReport() ? 0x04 : 0x00;
+        resultReportConfig |= params.hasAngleOfArrivalFigureOfMeritReport() ? 0x08 : 0x00;
+        return resultReportConfig;
+    }
+
+    private int getRangingRoundControl(FiraOpenSessionParams params) {
+        //RANGING_ROUND_CONTROL
+        int rangingRoundControl = 0x02;
+
+        // b0 : Ranging Result Report Message
+        rangingRoundControl |= params.hasResultReportPhase() ? 0x01 : 0x00;
+
+        // b7 : Measurement Report Message
+        if (params.getMeasurementReportType()
+                == FiraParams.MEASUREMENT_REPORT_TYPE_RESPONDER_TO_INITIATOR) {
+            rangingRoundControl |= 0x80;
+        }
+        return rangingRoundControl;
+    }
+}
diff --git a/service/java/com/android/server/uwb/params/GenericDecoder.java b/service/java/com/android/server/uwb/params/GenericDecoder.java
new file mode 100644
index 0000000..1c6a600
--- /dev/null
+++ b/service/java/com/android/server/uwb/params/GenericDecoder.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_POWER_STATS_QUERY;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.ccc.CccSpecificationParams;
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraSpecificationParams;
+import com.google.uwb.support.generic.GenericSpecificationParams;
+
+public class GenericDecoder extends TlvDecoder {
+    @Override
+    public <T extends Params> T getParams(TlvDecoderBuffer tlvs, Class<T> paramType) {
+        if (GenericSpecificationParams.class.equals(paramType)) {
+            return (T) getSpecificationParamsFromTlvBuffer(tlvs);
+        }
+        return null;
+    }
+
+    private GenericSpecificationParams getSpecificationParamsFromTlvBuffer(TlvDecoderBuffer tlvs) {
+        GenericSpecificationParams.Builder builder = new GenericSpecificationParams.Builder();
+        FiraSpecificationParams firaSpecificationParams =
+                TlvDecoder.getDecoder(FiraParams.PROTOCOL_NAME).getParams(
+                        tlvs, FiraSpecificationParams.class);
+        builder.setFiraSpecificationParams(firaSpecificationParams);
+        CccSpecificationParams cccSpecificationParams =
+                TlvDecoder.getDecoder(CccParams.PROTOCOL_NAME).getParams(
+                        tlvs, CccSpecificationParams.class);
+        builder.setCccSpecificationParams(cccSpecificationParams);
+        try {
+            byte supported_power_stats_query = tlvs.getByte(SUPPORTED_POWER_STATS_QUERY);
+            if (supported_power_stats_query != 0) {
+                builder.hasPowerStatsSupport(true);
+            }
+        } catch (IllegalArgumentException e) {
+            // Do nothing. By default, hasPowerStatsSupport() returns false.
+        }
+        return builder.build();
+    }
+}
diff --git a/service/java/com/android/server/uwb/params/TlvBuffer.java b/service/java/com/android/server/uwb/params/TlvBuffer.java
new file mode 100644
index 0000000..6a6ce95
--- /dev/null
+++ b/service/java/com/android/server/uwb/params/TlvBuffer.java
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import com.android.server.uwb.config.ConfigParam;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+/***
+ * This assumes little endian data and 1 byte tags. This is intended for handling UCI interface
+ * data.
+ */
+public class TlvBuffer {
+    private static final String TAG = "TlvBuffer";
+    private static final int MAX_BUFFER_SIZE = 512;
+    private final ByteBuffer mBuffer;
+    private final int mNoOfParams;
+
+    public TlvBuffer(byte[] tlvArray, int noOfParams) {
+        mBuffer = ByteBuffer.wrap(tlvArray);
+        mNoOfParams = noOfParams;
+    }
+
+    public byte[] getByteArray() {
+        return mBuffer.array();
+    }
+
+    public int getNoOfParams() {
+        return mNoOfParams;
+    }
+
+    public static final class Builder {
+        ByteBuffer mBuffer = ByteBuffer.allocate(MAX_BUFFER_SIZE);
+        int mNoOfParams = 0;
+        ByteOrder mOrder = ByteOrder.BIG_ENDIAN;
+
+        public TlvBuffer.Builder putOrder(ByteOrder order) {
+            mOrder = order;
+            return this;
+        }
+
+        public TlvBuffer.Builder putByte(int tagType, byte b) {
+            addHeader(tagType, Byte.BYTES);
+            this.mBuffer.put(b);
+            this.mNoOfParams++;
+            return this;
+        }
+
+        public TlvBuffer.Builder putByteArray(int tagType, byte[] bArray) {
+            return putByteArray(tagType, bArray.length, bArray);
+        }
+
+        public TlvBuffer.Builder putByteArray(int tagType, int length, byte[] bArray) {
+            addHeader(tagType, length);
+            this.mBuffer.put(bArray);
+            this.mNoOfParams++;
+            return this;
+        }
+
+        public TlvBuffer.Builder putShort(int tagType, short data) {
+            addHeader(tagType, Short.BYTES);
+            this.mBuffer.put(TlvUtil.getLeBytes(data));
+            this.mNoOfParams++;
+            return this;
+        }
+
+        public TlvBuffer.Builder putInt(int tagType, int data) {
+            addHeader(tagType, Integer.BYTES);
+            this.mBuffer.put(TlvUtil.getLeBytes(data));
+            this.mNoOfParams++;
+            return this;
+        }
+
+        public TlvBuffer.Builder putLong(int tagType, long data) {
+            addHeader(tagType, Long.BYTES);
+            this.mBuffer.put(TlvUtil.getLeBytes(data));
+
+            this.mNoOfParams++;
+            return this;
+        }
+
+        public TlvBuffer build() {
+            return new TlvBuffer(Arrays.copyOf(this.mBuffer.array(), this.mBuffer.position()),
+                    this.mNoOfParams);
+        }
+
+        private void addHeader(int tagType, int length) {
+            mBuffer.put(ConfigParam.getTagBytes(tagType));
+            mBuffer.put((byte) length);
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/params/TlvDecoder.java b/service/java/com/android/server/uwb/params/TlvDecoder.java
new file mode 100644
index 0000000..0fc1095
--- /dev/null
+++ b/service/java/com/android/server/uwb/params/TlvDecoder.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.generic.GenericParams;
+
+public abstract class TlvDecoder {
+    public static TlvDecoder getDecoder(String protocolName) {
+        if (protocolName.equals(FiraParams.PROTOCOL_NAME)) {
+            return new FiraDecoder();
+        }
+        if (protocolName.equals(CccParams.PROTOCOL_NAME)) {
+            return new CccDecoder();
+        }
+        if (protocolName.equals(GenericParams.PROTOCOL_NAME)) {
+            return new GenericDecoder();
+        }
+        return null;
+    }
+
+    public abstract <T extends Params> T getParams(TlvDecoderBuffer tlvs, Class<T> paramType);
+}
diff --git a/service/java/com/android/server/uwb/params/TlvDecoderBuffer.java b/service/java/com/android/server/uwb/params/TlvDecoderBuffer.java
new file mode 100644
index 0000000..9fc41e8
--- /dev/null
+++ b/service/java/com/android/server/uwb/params/TlvDecoderBuffer.java
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.uwb.config.ConfigParam;
+import com.android.server.uwb.util.UwbUtil;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+
+/***
+ * This assumes little endian data and 1 byte tags. This is intended for handling UCI interface
+ * data.
+ * @see com.android.server.uwb.secure.iso7816.TlvParser
+ */
+public class TlvDecoderBuffer {
+    private static final String TAG = "TlvDecoderBuffer";
+    private final ByteBuffer mBuffer;
+    private final int mNumParams;
+    private final Map<Byte, Tlv> mTlvs = new ArrayMap<>();
+
+    @VisibleForTesting
+    public static class Tlv {
+        public final byte tagType;
+        public final byte length;
+        public final byte[] value;
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof Tlv)) return false;
+            Tlv tlv = (Tlv) o;
+            return tagType == tlv.tagType && length == tlv.length && Arrays.equals(value,
+                    tlv.value);
+        }
+
+        @Override
+        public int hashCode() {
+            int result = Objects.hash(tagType, length);
+            result = 31 * result + Arrays.hashCode(value);
+            return result;
+        }
+
+        Tlv(byte tagType, byte length, byte[] value) {
+            this.tagType = tagType;
+            this.length = length;
+            this.value = value;
+        }
+
+        @Override
+        public String toString() {
+            return "Tlv[tagType: " + tagType + ", length: " + length + ", value: "
+                    + UwbUtil.toHexString(value) + "]";
+        }
+    }
+
+    public TlvDecoderBuffer(byte[] tlvArray, int noOfParams) {
+        mBuffer = ByteBuffer.wrap(tlvArray);
+        mNumParams = noOfParams;
+    }
+
+    @VisibleForTesting
+    public byte[] getByteArray() {
+        return mBuffer.array();
+    }
+
+    @VisibleForTesting
+    public int getNumParams() {
+        return mNumParams;
+    }
+
+    @VisibleForTesting
+    public Collection<Tlv> getTlvs() {
+        return mTlvs.values();
+    }
+
+    public boolean parse() {
+        if (mBuffer.capacity() == 0) return false;
+        while (mBuffer.hasRemaining()) {
+            try {
+                byte tagType = mBuffer.get();
+                byte length = mBuffer.get();
+                byte[] value = new byte[length];
+                mBuffer.get(value);
+                Log.i(TAG, "Parsed TLV: " + new Tlv(tagType, length, value));
+                mTlvs.put(tagType, new Tlv(tagType, length, value));
+            } catch (BufferUnderflowException e) {
+                Log.e(TAG, "Failed to parse buffer at position: " + mBuffer.position(), e);
+                return false;
+            }
+        }
+        if (mNumParams != mTlvs.size()) {
+            Log.e(TAG, "Num TLVs parsed does not equal the num params, tlvs: " + mTlvs.size()
+                    + ", num params: " + mNumParams);
+            return false;
+        }
+        return true;
+    }
+
+    @Nullable
+    private Tlv getTlv(int tagType) {
+        byte[] tagTypeByte = ConfigParam.getTagBytes(tagType);
+        if (tagTypeByte.length > 1) {
+            throw new IllegalArgumentException("Invalid tagType: " + tagTypeByte);
+        }
+        Tlv tlv = mTlvs.get(tagTypeByte[0]);
+        if (tlv == null) {
+            throw new IllegalArgumentException("Tag type: " + tagType + " not present");
+        }
+        return tlv;
+    }
+
+    public Byte getByte(int tagType) {
+        Tlv tlv = getTlv(tagType);
+        if (tlv.length != Byte.BYTES) {
+            throw new IllegalArgumentException(
+                    "Mismatch in value type, expected byte found len: " + tlv.length);
+        }
+        try {
+            return ByteBuffer.wrap(tlv.value).get();
+        } catch (BufferUnderflowException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    public Short getShort(int tagType) {
+        Tlv tlv = getTlv(tagType);
+        if (tlv.length != Short.BYTES) {
+            throw new IllegalArgumentException(
+                    "Mismatch in value type, expected short found len: " + tlv.length);
+        }
+        try {
+            return ByteBuffer.wrap(tlv.value).order(ByteOrder.LITTLE_ENDIAN).getShort();
+        } catch (BufferUnderflowException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    public Integer getInt(int tagType) {
+        Tlv tlv = getTlv(tagType);
+        if (tlv.length != Integer.BYTES) {
+            throw new IllegalArgumentException(
+                    "Mismatch in value type, expected int found len: " + tlv.length);
+        }
+        try {
+            return ByteBuffer.wrap(tlv.value).order(ByteOrder.LITTLE_ENDIAN).getInt();
+        } catch (BufferUnderflowException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    public Long getLong(int tagType) {
+        Tlv tlv = getTlv(tagType);
+        if (tlv.length != Long.BYTES) {
+            throw new IllegalArgumentException(
+                    "Mismatch in value long, expected int found len: " + tlv.length);
+        }
+        try {
+            return ByteBuffer.wrap(tlv.value).order(ByteOrder.LITTLE_ENDIAN).getLong();
+        } catch (BufferUnderflowException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    public byte[] getByteArray(int tagType) {
+        Tlv tlv = getTlv(tagType);
+        byte[] value = new byte[tlv.length];
+        try {
+            ByteBuffer.wrap(tlv.value).get(value);
+            return value;
+        } catch (BufferUnderflowException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/params/TlvEncoder.java b/service/java/com/android/server/uwb/params/TlvEncoder.java
new file mode 100644
index 0000000..8902043
--- /dev/null
+++ b/service/java/com/android/server/uwb/params/TlvEncoder.java
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.fira.FiraParams;
+
+public abstract class TlvEncoder {
+    public static TlvEncoder getEncoder(String protocolName) {
+        if (protocolName.equals(FiraParams.PROTOCOL_NAME)) {
+            return new FiraEncoder();
+        }
+        if (protocolName.equals(CccParams.PROTOCOL_NAME)) {
+            return new CccEncoder();
+        }
+        return null;
+    }
+
+    public abstract TlvBuffer getTlvBuffer(Params param);
+}
diff --git a/service/java/com/android/server/uwb/params/TlvUtil.java b/service/java/com/android/server/uwb/params/TlvUtil.java
new file mode 100644
index 0000000..8b35f1f
--- /dev/null
+++ b/service/java/com/android/server/uwb/params/TlvUtil.java
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class TlvUtil {
+    public static final byte[] getBytes(byte data) {
+        ByteBuffer buffer = ByteBuffer.allocate(Byte.BYTES).put(data);
+        return buffer.array();
+    }
+
+    public static final byte[] getBytes(short data) {
+        ByteBuffer buffer = ByteBuffer.allocate(Short.BYTES).order(
+                ByteOrder.BIG_ENDIAN).putShort(data);
+        return buffer.array();
+    }
+
+    public static final byte[] getLeBytes(short data) {
+        ByteBuffer buffer = ByteBuffer.allocate(Short.BYTES).order(
+                ByteOrder.LITTLE_ENDIAN).putShort(data);
+        return buffer.array();
+    }
+
+    public static final byte[] getBytes(int data) {
+        ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES).order(
+                ByteOrder.BIG_ENDIAN).putInt(data);
+        return buffer.array();
+    }
+
+    public static final byte[] getLeBytes(int data) {
+        ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES).order(
+                ByteOrder.LITTLE_ENDIAN).putInt(data);
+        return buffer.array();
+    }
+
+    public static final byte[] getBytes(long data) {
+        ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES).order(
+                ByteOrder.BIG_ENDIAN).putLong(data);
+        return buffer.array();
+    }
+
+    public static final byte[] getLeBytes(long data) {
+        ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES).order(
+                ByteOrder.LITTLE_ENDIAN).putLong(data);
+        return buffer.array();
+    }
+
+    public static final byte[] getReverseBytes(byte[] data) {
+        byte[] buffer = new byte[data.length];
+        for (int i = 0; i < data.length; i++) {
+            buffer[i] = data[data.length - 1 - i];
+        }
+        return buffer;
+    }
+
+    public static final byte[] getBytes(int data, int start, int length) {
+        ByteBuffer srcBuf = ByteBuffer.allocate(Integer.BYTES).putInt(data);
+        ByteBuffer dstBuf = ByteBuffer.allocate(length);
+        srcBuf.position(start);
+        dstBuf.put(srcBuf);
+        return dstBuf.array();
+    }
+
+    public static final byte[] getBytesWithLeftPadding(int size, byte[] data) {
+        ByteBuffer buffer = ByteBuffer.allocate(size);
+        int startOffset = size - data.length;
+        buffer.position(startOffset);
+        buffer.put(data);
+        return buffer.array();
+    }
+
+    public static final byte[] getBytesWithRightPadding(int size, byte[] data) {
+        ByteBuffer buffer = ByteBuffer.allocate(size).put(data);
+        return buffer.array();
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/SecureElementChannel.java b/service/java/com/android/server/uwb/secure/SecureElementChannel.java
new file mode 100644
index 0000000..875ca8b
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/SecureElementChannel.java
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure;
+
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_NO_ERROR;
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_NO_SPECIFIC_DIAGNOSTIC;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
+
+import com.android.server.uwb.secure.csml.FiRaCommand;
+import com.android.server.uwb.secure.iso7816.CommandApdu;
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.omapi.OmapiConnection;
+import com.android.server.uwb.secure.omapi.OmapiConnection.InitCompletionCallback;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/** Manages the Secure Element and allows communications with the FiRa applet. */
+@WorkerThread
+public class SecureElementChannel {
+    private static final String LOG_TAG = "SecureElementChannel";
+    private static final int MAX_SE_OPERATION_RETRIES = 3;
+    private static final int DELAY_BETWEEN_SE_RETRY_ATTEMPTS_MILLIS = 10;
+
+    private static final StatusWord SW_TEMPORARILY_UNAVAILABLE =
+            StatusWord.SW_CONDITIONS_NOT_SATISFIED;
+
+    private final OmapiConnection mOmapiConnection;
+    private final boolean mRemoveDelayBetweenRetriesForTest;
+
+    private boolean mIsOpened = false;
+
+    /**
+     * The constructor of the SecureElementChannel.
+     */
+    public SecureElementChannel(@NonNull OmapiConnection omapiConnection) {
+        this(omapiConnection, /* removeDelayBetweenRetries= */ false);
+    }
+
+    // This constructor is made visible because we need to remove the delay between SE operations
+    // during tests. Calling Thread.sleep in tests actually causes the thread running the test to
+    // sleep and leads to the test timing out.
+    @VisibleForTesting
+    SecureElementChannel(
+            @NonNull OmapiConnection omapiConnection,
+            boolean removeDelayBetweenRetriesForTest) {
+        this.mOmapiConnection = omapiConnection;
+        this.mRemoveDelayBetweenRetriesForTest = removeDelayBetweenRetriesForTest;
+    }
+
+    /**
+     * Initializes the SecureElementChannel.
+     */
+    public void init(@NonNull InitCompletionCallback callback) {
+        mOmapiConnection.init(callback::onInitCompletion);
+    }
+
+    /**
+     * Opens the channel to the FiRa applet, true if success.
+     */
+    public boolean openChannel() {
+        try {
+            ResponseApdu responseApdu = openChannelWithResponse();
+            if (responseApdu.getStatusWord() != SW_NO_ERROR.toInt()) {
+                logw("Received error [" + responseApdu + "] while opening channel");
+                return false;
+            }
+        } catch (IOException e) {
+            loge("Encountered exception while opening channel" + e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Opens the channel to the FiRa applet, returns the Response APDU.
+     */
+    @NonNull
+    public ResponseApdu openChannelWithResponse() throws IOException {
+        ResponseApdu responseApdu = ResponseApdu.fromStatusWord(SW_TEMPORARILY_UNAVAILABLE);
+        for (int i = 0; i < MAX_SE_OPERATION_RETRIES; i++) {
+            responseApdu = mOmapiConnection.openChannel();
+
+            if (!shouldRetryOpenChannel(responseApdu)) {
+                break;
+            }
+
+            logw("Open channel failed because SE is temporarily unavailable. "
+                    + "Total attempts so far: " + (i + 1));
+
+            threadSleep(DELAY_BETWEEN_SE_RETRY_ATTEMPTS_MILLIS);
+        }
+
+        if (responseApdu.getStatusWord() == StatusWord.SW_NO_ERROR.toInt()) {
+            mIsOpened = true;
+        } else {
+            logw("All open channel attempts failed!");
+        }
+        return responseApdu;
+    }
+
+    /**
+     * Checks if current channel is opened or not.
+     */
+    public boolean isOpened() {
+        return mIsOpened;
+    }
+
+    private boolean shouldRetryOpenChannel(ResponseApdu responseApdu) {
+        return Arrays.asList(SW_TEMPORARILY_UNAVAILABLE, SW_NO_SPECIFIC_DIAGNOSTIC)
+                .contains(StatusWord.fromInt(responseApdu.getStatusWord()));
+    }
+
+    /**
+     * Closes the channel to the FiRa applet.
+     * @return
+     */
+    public boolean closeChannel() {
+        try {
+            mOmapiConnection.closeChannel();
+        } catch (IOException e) {
+            logw("Encountered exception while closing channel" + e);
+            return false;
+        }
+        mIsOpened = false;
+        return true;
+    }
+
+    /**
+     * Transmits a Command APDU defined by the FiRa to the FiRa applet.
+     */
+    @NonNull
+    public ResponseApdu transmit(FiRaCommand fiRaCommand) throws IOException {
+        return transmit(fiRaCommand.getCommandApdu());
+    }
+
+    /**
+     * Transmits a Command APDU to FiRa applet.
+     */
+    @NonNull
+    public ResponseApdu transmit(CommandApdu command)
+            throws IOException {
+        ResponseApdu responseApdu = ResponseApdu.fromStatusWord(SW_TEMPORARILY_UNAVAILABLE);
+
+        if (!mIsOpened) {
+            return responseApdu;
+        }
+        for (int i = 0; i < MAX_SE_OPERATION_RETRIES; i++) {
+            responseApdu = mOmapiConnection.transmit(command);
+            if (responseApdu.getStatusWord() != SW_TEMPORARILY_UNAVAILABLE.toInt()) {
+                return responseApdu;
+            }
+            logw("Transmit failed because SE is temporarily unavailable. "
+                    + "Total attempts so far: " + (i + 1));
+            threadSleep(DELAY_BETWEEN_SE_RETRY_ATTEMPTS_MILLIS);
+        }
+        logw("All transmit attempts for SE failed!");
+        return responseApdu;
+    }
+
+
+    private void threadSleep(long millis) {
+        if (!mRemoveDelayBetweenRetriesForTest) {
+            try {
+                Thread.sleep(millis);
+            } catch (InterruptedException e) {
+                logw("Thread sleep interrupted.");
+            }
+        }
+    }
+
+    private void logw(String log) {
+        Log.w(LOG_TAG, log);
+    }
+
+    private void loge(String log) {
+        Log.e(LOG_TAG, log);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/CsmlUtil.java b/service/java/com/android/server/uwb/secure/csml/CsmlUtil.java
new file mode 100644
index 0000000..03add36
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/CsmlUtil.java
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.android.server.uwb.secure.iso7816.Iso7816Constants.EXTENDED_HEAD_LIST;
+import static com.android.server.uwb.secure.iso7816.Iso7816Constants.TAG_LIST;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+import com.android.server.uwb.util.ObjectIdentifier;
+
+import com.google.common.primitives.Bytes;
+
+/**
+ * Utils created for working with {@link AtomicFile}.
+ */
+public final class CsmlUtil {
+    private CsmlUtil() {}
+
+    static final Tag OID_TAG = new Tag((byte) 0x06);
+    private static final Tag TAG_LIST_TAG = new Tag(TAG_LIST);
+    private static final Tag EXTENDED_HEAD_LIST_TAG = new Tag(EXTENDED_HEAD_LIST);
+    // FiRa CSML 8.2.2.7.1.4
+    private static final Tag TERMINATE_SESSION_DO_TAG = new Tag((byte) 0x80);
+    private static final Tag TERMINATE_SESSION_TOP_DO_TAG = new Tag((byte) 0xBF, (byte) 0x79);
+
+    /**
+     * Encode the {@link ObjectIdentifier} as TLV format, which is used as the payload of TlvDatum
+     * @param oid the ObjectIdentifier
+     * @return The instance of TlvDatum.
+     */
+    @NonNull
+    public static TlvDatum encodeObjectIdentifierAsTlv(@NonNull ObjectIdentifier oid) {
+        return new TlvDatum(OID_TAG, oid.value);
+    }
+
+    /**
+     * Construct the TLV payload for {@link getDoCommand} with
+     * TAG LIST defined in ISO7816-4.
+     */
+    @NonNull
+    public static TlvDatum constructGetDoTlv(@NonNull Tag tag) {
+        return new TlvDatum(TAG_LIST_TAG, tag.literalValue);
+    }
+
+    /**
+     * Get the TLV for terminate session command.
+     */
+    @NonNull
+    public static TlvDatum constructTerminateSessionGetDoTlv() {
+        // TODO: confirm the structure defined in CSML 8.2.2.7.1.4, which is not clear.
+        byte[] value = constructDeepestTagOfGetDoAllContent(TERMINATE_SESSION_DO_TAG);
+        return constructGetOrPutDoTlv(
+                new TlvDatum(TERMINATE_SESSION_TOP_DO_TAG, value));
+    }
+
+    /**
+     * Construct the TLV payload for @link getDoCommand} with
+     * EXTENTED HEADER LIST defined in ISO7816-4.
+     */
+    @NonNull
+    public static TlvDatum constructGetOrPutDoTlv(TlvDatum tlvDatum) {
+        return new TlvDatum(EXTENDED_HEAD_LIST_TAG, tlvDatum);
+    }
+
+    /**
+     * Get all content for a specific/deepest Tag in the DO tree with Extented Header List.
+     */
+    @NonNull
+    public static byte[] constructDeepestTagOfGetDoAllContent(Tag tag) {
+        return Bytes.concat(tag.literalValue, new byte[] {(byte) 0x00});
+    }
+
+    /**
+     * Get part of content for a specific/deepest Tag with Extenteed Header List.
+     */
+    @NonNull
+    public static byte[] constructDeepestTagOfGetDoPartContent(Tag tag, int len) {
+        if (len > 256) {
+            throw new IllegalArgumentException("The content length can not be over 256 bytes");
+        }
+
+        return Bytes.concat(tag.literalValue, new byte[] { (byte) len});
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/DeleteAdfCommand.java b/service/java/com/android/server/uwb/secure/csml/DeleteAdfCommand.java
new file mode 100644
index 0000000..1329313
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/DeleteAdfCommand.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.util.ObjectIdentifier;
+
+import java.util.Arrays;
+import java.util.List;
+
+// TODO: this is customized, need to make it be standardized in CSML
+/**
+ * Delete ADF C-APDU.
+ */
+public class DeleteAdfCommand extends FiRaCommand {
+    private final ObjectIdentifier mAdfOid;
+
+    private DeleteAdfCommand(@NonNull ObjectIdentifier adfOid) {
+        super();
+        this.mAdfOid = adfOid;
+    }
+
+    @Override
+    protected byte getIns() {
+        return (byte) 0xE4;
+    }
+
+    @Override
+    @NonNull
+    protected StatusWord[] getExpectedSw() {
+        return new StatusWord[] {
+                StatusWord.SW_NO_ERROR,
+                StatusWord.SW_WARNING_STATE_UNCHANGED, // OID not found,
+                StatusWord.SW_WRONG_LENGTH,
+                StatusWord.SW_CONDITIONS_NOT_SATISFIED,
+                StatusWord.SW_FUNCTION_NOT_SUPPORTED,
+                StatusWord.SW_WRONG_DATA,
+                StatusWord.SW_INCORRECT_P1P2 };
+    }
+
+    @Override
+    @NonNull
+    protected List<TlvDatum> getTlvPayload() {
+        return Arrays.asList(CsmlUtil.encodeObjectIdentifierAsTlv(mAdfOid));
+    }
+
+    /**
+     * Builds the APDU command of DeleteAdfCommand.
+     */
+    @NonNull
+    public static DeleteAdfCommand build(@NonNull ObjectIdentifier adfOid) {
+        return new DeleteAdfCommand(adfOid);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/DeleteAdfResponse.java b/service/java/com/android/server/uwb/secure/csml/DeleteAdfResponse.java
new file mode 100644
index 0000000..11d69bd
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/DeleteAdfResponse.java
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.csml;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+
+/**
+ * See CSML 1.0 - 8.2.2.14.1.4 DELETE ADF
+ */
+public class DeleteAdfResponse extends FiRaResponse {
+
+    private DeleteAdfResponse(@NonNull ResponseApdu responseApdu) {
+        super(responseApdu.getStatusWord());
+    }
+
+    /**
+     * Parse the response of DeleteAdfCommand.
+     */
+    public static DeleteAdfResponse fromResponseApdu(@NonNull ResponseApdu responseApdu) {
+        return new DeleteAdfResponse(responseApdu);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/DispatchCommand.java b/service/java/com/android/server/uwb/secure/csml/DispatchCommand.java
new file mode 100644
index 0000000..e55bc14
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/DispatchCommand.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Dispatch C-APDU, see CSML 1.0 8.2.2.14.2.9
+ */
+public class DispatchCommand extends FiRaCommand {
+    private static final Tag DISPATCH_DATA_TAG = new Tag((byte) 0x81);
+
+    @NonNull
+    private final byte[] mDispatchData;
+
+    private DispatchCommand(@NonNull byte[] dispatchData) {
+        super();
+        mDispatchData = dispatchData;
+    }
+
+    @Override
+    protected byte getIns() {
+        return (byte) 0xC2;
+    }
+
+    @Override
+    @NonNull
+    protected StatusWord[] getExpectedSw() {
+        return new StatusWord[] {
+                StatusWord.SW_NO_ERROR,
+                StatusWord.SW_CONDITIONS_NOT_SATISFIED,
+                StatusWord.SW_FUNCTION_NOT_SUPPORTED,
+                StatusWord.SW_INCORRECT_P1P2 };
+    }
+
+    @Override
+    @NonNull
+    protected List<TlvDatum> getTlvPayload() {
+        return Arrays.asList(
+                new TlvDatum(FIRA_PROPRIETARY_COMMAND_TEMP_TAG,
+                        new TlvDatum(DISPATCH_DATA_TAG, mDispatchData)));
+    }
+
+    /**
+     * Builds the DispatchCommand.
+     */
+    @NonNull
+    public static DispatchCommand build(@NonNull byte[] dispatchData) {
+        return new DispatchCommand(dispatchData);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/DispatchResponse.java b/service/java/com/android/server/uwb/secure/csml/DispatchResponse.java
new file mode 100644
index 0000000..18e89b9
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/DispatchResponse.java
@@ -0,0 +1,363 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.csml;
+
+import static com.android.server.uwb.secure.csml.FiRaResponse.PROPRIETARY_RESPONSE_TAG;
+
+import android.annotation.IntDef;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+import com.android.server.uwb.secure.iso7816.TlvParser;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+import com.android.server.uwb.util.ObjectIdentifier;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Response of Dispatch APDU, See CSML 1.0 - 8.2.2.14.2.9
+ */
+public class DispatchResponse extends FiRaResponse {
+    @VisibleForTesting
+    static final Tag STATUS_TAG = new Tag((byte) 0x80);
+    @VisibleForTesting
+    static final Tag DATA_TAG = new Tag((byte) 0x81);
+    @VisibleForTesting
+    static final Tag NOTIFICATION_TAG = new Tag((byte) 0xE1);
+    @VisibleForTesting
+    static final Tag NOTIFICATION_FORMAT_TAG = new Tag((byte) 0x80);
+    @VisibleForTesting
+    static final Tag NOTIFICATION_EVENT_ID_TAG = new Tag((byte) 0x81);
+    @VisibleForTesting
+    static final Tag NOTIFICATION_DATA_TAG = new Tag((byte) 0x82);
+
+    @IntDef(prefix = { "TRANSACTION_STATUS_" }, value = {
+            TRANSACTION_STATUS_NO_ERROR,
+            TRANSACTION_STATUS_FORWARD_TO_REMOTE,
+            TRANSACTION_STATUS_FORWARD_TO_HOST_APP,
+            TRANSACTION_STATUS_WITH_ERROR,
+            TRANSACTION_STATUS_NO_OP,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface TransctionStatus {}
+
+    private static final int TRANSACTION_STATUS_NO_ERROR = 0;
+    private static final int TRANSACTION_STATUS_FORWARD_TO_REMOTE = 1;
+    private static final int TRANSACTION_STATUS_FORWARD_TO_HOST_APP = 2;
+    private static final int TRANSACTION_STATUS_WITH_ERROR = 3;
+    private static final int TRANSACTION_STATUS_NO_OP = 4;
+
+    @IntDef(prefix = { "NOTIFICATION_EVENT_ID_" }, value = {
+            NOTIFICATION_EVENT_ID_ADF_SELECTED,
+            NOTIFICATION_EVENT_ID_SECURE_CHANNEL_ESTABLISHED,
+            NOTIFICATION_EVENT_ID_RDS_AVAILABLE,
+            NOTIFICATION_EVENT_ID_SECURE_SESSION_ABORTED,
+            NOTIFICATION_EVENT_ID_SEURE_SESSION_AUTO_TERMINATED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface NotificationEventId {}
+
+    public static final int NOTIFICATION_EVENT_ID_ADF_SELECTED = 0;
+    public static final int NOTIFICATION_EVENT_ID_SECURE_CHANNEL_ESTABLISHED = 1;
+    public static final int NOTIFICATION_EVENT_ID_RDS_AVAILABLE = 2;
+    public static final int NOTIFICATION_EVENT_ID_SECURE_SESSION_ABORTED = 3;
+    public static final int NOTIFICATION_EVENT_ID_SEURE_SESSION_AUTO_TERMINATED = 4;
+
+    /**
+     * The base class of notification from the FiRa applet.
+     */
+    public static class Notification {
+        @NotificationEventId
+        public final int notificationEventId;
+
+        protected Notification(@NotificationEventId int notificationEventId) {
+            this.notificationEventId = notificationEventId;
+        }
+    }
+
+    /**
+     * The notification of ADF selected.
+     */
+    public static class AdfSelectedNotification extends Notification {
+        @NonNull
+        public final ObjectIdentifier adfOid;
+
+        private AdfSelectedNotification(@NonNull ObjectIdentifier adfOid) {
+            super(NOTIFICATION_EVENT_ID_ADF_SELECTED);
+
+            this.adfOid = adfOid;
+        }
+    }
+
+    /**
+     * The notification of the secure channel established.
+     */
+    public static class SecureChannelEstablishedNotification extends Notification {
+        private SecureChannelEstablishedNotification() {
+            super(NOTIFICATION_EVENT_ID_SECURE_CHANNEL_ESTABLISHED);
+        }
+    }
+
+    /**
+     * The notification of the secure session aborted for internal error.
+     */
+    public static class SecureSessionAbortedNotification extends Notification {
+        private SecureSessionAbortedNotification() {
+            super(NOTIFICATION_EVENT_ID_SECURE_SESSION_ABORTED);
+        }
+    }
+
+    /**
+     * The notification of the secure session terminated automatically.
+     */
+    public static class SecureSessionAutoTerminatedNotification extends Notification {
+        private SecureSessionAutoTerminatedNotification() {
+            super(NOTIFICATION_EVENT_ID_SEURE_SESSION_AUTO_TERMINATED);
+        }
+    }
+
+    /**
+     * The notification of RDS available to be used.
+     */
+    public static class RdsAvailableNotification extends Notification {
+        public final int sessionId;
+
+        @NonNull
+        public final Optional<byte[]> arbitraryData;
+
+        private RdsAvailableNotification(
+                int sessionId, @Nullable byte[] arbitraryData) {
+            super(NOTIFICATION_EVENT_ID_RDS_AVAILABLE);
+            this.sessionId = sessionId;
+            if (arbitraryData == null) {
+                this.arbitraryData = Optional.empty();
+            } else {
+                this.arbitraryData = Optional.of(arbitraryData);
+            }
+        }
+    }
+
+    @TransctionStatus
+    private int mTransactionStatus = TRANSACTION_STATUS_NO_OP;
+
+    /**
+     * The data should be sent to the peer device or host app.
+     */
+    @NonNull
+    private Optional<OutboundData> mOutboundData = Optional.empty();
+
+    public Optional<OutboundData> getOutboundData() {
+        return mOutboundData;
+    }
+
+    /**
+     * The notifications got from the Dispatch response.
+     */
+    @NonNull
+    public final List<Notification> notifications;
+
+    private DispatchResponse(@NonNull ResponseApdu responseApdu) {
+        super(responseApdu.getStatusWord());
+        notifications = new ArrayList<Notification>();
+        if (!isSuccess()) {
+            return;
+        }
+        Map<Tag, List<TlvDatum>> proprietaryTlvsMap = TlvParser.parseTlvs(responseApdu);
+        List<TlvDatum> proprietaryTlv = proprietaryTlvsMap.get(PROPRIETARY_RESPONSE_TAG);
+        if (proprietaryTlv.size() == 0) {
+            return;
+        }
+
+        Map<Tag, List<TlvDatum>> tlvsMap = TlvParser.parseTlvs(proprietaryTlv.get(0).value);
+
+        notifications.addAll(parseNotification(tlvsMap.get(NOTIFICATION_TAG)));
+
+        List<TlvDatum> statusTlvs = tlvsMap.get(STATUS_TAG);
+        if (statusTlvs == null || statusTlvs.size() == 0) {
+            // no status attached.
+            return;
+        }
+        mTransactionStatus = parseTransctionStatus(statusTlvs.get(0).value);
+        switch (mTransactionStatus) {
+            case TRANSACTION_STATUS_NO_ERROR:
+                notifications.add(new SecureSessionAutoTerminatedNotification());
+                break;
+            case TRANSACTION_STATUS_WITH_ERROR:
+                notifications.add(new SecureSessionAbortedNotification());
+                break;
+            case TRANSACTION_STATUS_FORWARD_TO_HOST_APP:
+                // fall through
+            case TRANSACTION_STATUS_FORWARD_TO_REMOTE:
+                List<TlvDatum> dataTlvs = tlvsMap.get(DATA_TAG);
+                if (dataTlvs.size() == 0) {
+                    break;
+                }
+                if (mTransactionStatus == TRANSACTION_STATUS_FORWARD_TO_HOST_APP) {
+                    mOutboundData = Optional.of(
+                            new OutboundData(OUTBOUND_TARGET_HOST_APP,
+                                    dataTlvs.get(0).value));
+                } else {
+                    mOutboundData = Optional.of(
+                            new OutboundData(OUTBOUND_TARGET_REMOTE,
+                                    dataTlvs.get(0).value));
+                }
+                break;
+            case TRANSACTION_STATUS_NO_OP:
+                // fall through
+            default:
+                break;
+        }
+    }
+
+    @TransctionStatus
+    private int parseTransctionStatus(@Nullable byte[] status) {
+        if (status == null || status.length < 1) {
+            return TRANSACTION_STATUS_NO_OP;
+        }
+        switch (status[0]) {
+            case (byte) 0x00:
+                return TRANSACTION_STATUS_NO_ERROR;
+            case (byte) 0x80:
+                return TRANSACTION_STATUS_FORWARD_TO_REMOTE;
+            case (byte) 0x81:
+                return TRANSACTION_STATUS_FORWARD_TO_HOST_APP;
+            case (byte) 0xFF:
+                return TRANSACTION_STATUS_WITH_ERROR;
+            default:
+                return TRANSACTION_STATUS_NO_OP;
+        }
+    }
+
+    // throw IllegalStateException
+    @NonNull
+    private List<Notification> parseNotification(
+            @Nullable List<TlvDatum> notificationTlvs) {
+        List<Notification> notificationList = new ArrayList<>();
+        if (notificationTlvs == null || notificationTlvs.size() == 0) {
+            return notificationList;
+        }
+
+        for (TlvDatum tlv : notificationTlvs) {
+            Map<Tag, List<TlvDatum>> curTlvs = TlvParser.parseTlvs(tlv.value);
+            List<TlvDatum> eventIdTlvs = curTlvs.get(NOTIFICATION_EVENT_ID_TAG);
+            if (eventIdTlvs == null || eventIdTlvs.size() == 0) {
+                throw new IllegalStateException("Notification event ID is not available.");
+            }
+            byte[] eventIdValue = eventIdTlvs.get(0).value;
+            if (eventIdValue == null || eventIdValue.length == 0) {
+                throw new IllegalStateException("Notification event ID value is not available.");
+            }
+            switch (eventIdValue[0]) {
+                case (byte) 0x00:
+                    // parse OID
+                    List<TlvDatum> notificationDataTlvs = curTlvs.get(NOTIFICATION_DATA_TAG);
+                    if (notificationDataTlvs == null || notificationDataTlvs.size() == 0) {
+                        throw new IllegalStateException("Notification data - OID is not available");
+                    }
+
+                    byte[] adfOidBytes = notificationDataTlvs.get(0).value;
+                    ObjectIdentifier adfOid =
+                            ObjectIdentifier.fromBytes(adfOidBytes);
+
+                    notificationList.add(new AdfSelectedNotification(adfOid));
+                    break;
+                case (byte) 0x01:
+                    notificationList.add(new SecureChannelEstablishedNotification());
+                    break;
+                case (byte) 0x02:
+                    // parse sessionId and arbitrary data
+                    notificationDataTlvs = curTlvs.get(NOTIFICATION_DATA_TAG);
+                    if (notificationDataTlvs == null || notificationDataTlvs.size() == 0) {
+                        throw new IllegalStateException(
+                                "RDS Notification data - sessionId is not available");
+                    }
+                    byte[] payload = notificationDataTlvs.get(0).value;
+                    if (payload == null || payload.length < 2 || payload.length < 1 + payload[0]) {
+                        throw new IllegalStateException(
+                                "RDS Notificaition data - bad payload");
+                    }
+                    int sessionIdLen = payload[0];
+                    byte[] sessionId = new byte[sessionIdLen];
+                    System.arraycopy(payload, 1, sessionId, 0, sessionIdLen);
+
+                    byte[] arbitratryData = null;
+                    int arbitratryDataOffset = sessionIdLen + 1;
+                    if (payload.length > arbitratryDataOffset) {
+                        int arbitratryDataLen = payload[arbitratryDataOffset];
+                        if (payload.length != 2 + sessionIdLen + arbitratryDataLen) {
+                            // ignore the arbitrary data
+                            arbitratryData = null;
+                        } else {
+                            arbitratryData = new byte[arbitratryDataLen];
+                            System.arraycopy(payload, arbitratryDataOffset + 1,
+                                    arbitratryData, 0, arbitratryDataLen);
+                        }
+                    }
+
+                    notificationList.add(
+                            new RdsAvailableNotification(
+                                    DataTypeConversionUtil.arbitraryByteArrayToI32(sessionId),
+                                    arbitratryData));
+                    break;
+                default:
+            }
+        }
+
+        return notificationList;
+    }
+
+    /**
+     * Parse the response of InitiateTractionCommand.
+     */
+    @NonNull
+    public static DispatchResponse fromResponseApdu(@NonNull ResponseApdu responseApdu) {
+        return new DispatchResponse(responseApdu);
+    }
+
+    @IntDef(prefix = { "OUTBOUND_TARGET_" }, value = {
+            OUTBOUND_TARGET_HOST_APP,
+            OUTBOUND_TARGET_REMOTE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface OutboundTarget {}
+
+    public static final int OUTBOUND_TARGET_HOST_APP = 0;
+    public static final int OUTBOUND_TARGET_REMOTE = 1;
+
+    /**
+     * The outbound data from the DispatchResponse.
+     */
+    public static class OutboundData {
+        @OutboundTarget
+        public final int target;
+        public final byte[] data;
+
+        private OutboundData(@OutboundTarget int target, byte[] data) {
+            this.target = target;
+            this.data = data;
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/FiRaCommand.java b/service/java/com/android/server/uwb/secure/csml/FiRaCommand.java
new file mode 100644
index 0000000..08f1693
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/FiRaCommand.java
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.csml;
+
+import static com.android.server.uwb.secure.iso7816.Iso7816Constants.CLA_PROPRIETARY;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.CommandApdu;
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * The base class of all C-APDU defined by FiRa.
+ */
+public abstract class FiRaCommand {
+    public static final Tag FIRA_PROPRIETARY_COMMAND_TEMP_TAG =
+            new Tag((byte) 0x71);
+
+    protected FiRaCommand(){
+    }
+
+    protected byte getCla() {
+        // logical channel number is not available. Use the default value.
+        return CLA_PROPRIETARY;
+    }
+
+    protected abstract byte getIns();
+
+    protected byte getP1() {
+        return (byte) 0x00;
+    }
+
+    protected byte getP2() {
+        return (byte) 0x00;
+    }
+
+    protected byte getLe() {
+        return (byte) 0x00;
+    }
+
+    protected abstract StatusWord[] getExpectedSw();
+
+    @NonNull
+    protected abstract List<TlvDatum> getTlvPayload();
+
+    @NonNull
+    private byte[] buildPayload(@NonNull List<TlvDatum> tlvData) {
+        if (tlvData.size() == 0) {
+            return new byte[0];
+        }
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
+        try {
+            for (TlvDatum tlv : tlvData) {
+                dataOutputStream.write(tlv.toBytes());
+            }
+
+            dataOutputStream.flush();
+        } catch (IOException e) {
+            return new byte[0];
+        }
+        return byteArrayOutputStream.toByteArray();
+    }
+
+    /**
+     * Converts the FiRa command to the CommandApdu of ISO7816-4.
+     */
+    @NonNull
+    public CommandApdu getCommandApdu() {
+        CommandApdu commandApdu = CommandApdu.builder(getCla(), getIns(), getP1(), getP2())
+                .setCdata(buildPayload(getTlvPayload()))
+                .setLe(getLe())
+                .setExpected(getExpectedSw())
+                .build();
+        return commandApdu;
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/FiRaResponse.java b/service/java/com/android/server/uwb/secure/csml/FiRaResponse.java
new file mode 100644
index 0000000..0e5e957
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/FiRaResponse.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.csml;
+
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_NO_ERROR;
+
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+
+/**
+ * The base class of all responses for APDU commands defined by FiRa.
+ */
+public class FiRaResponse {
+    public static final Tag PROPRIETARY_RESPONSE_TAG = new Tag((byte) 0x71);
+
+    /**
+     * The sw of APDU response.
+     */
+    public final StatusWord statusWord;
+
+    protected FiRaResponse(int sw) {
+        this.statusWord = StatusWord.fromInt(sw);
+    }
+
+    /**
+     * Check if the APDU command is processed by the applet successfully.
+     */
+    public boolean isSuccess() {
+        return statusWord.equals(SW_NO_ERROR);
+    }
+
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/GetDoCommand.java b/service/java/com/android/server/uwb/secure/csml/GetDoCommand.java
new file mode 100644
index 0000000..68f3bf5
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/GetDoCommand.java
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Get data object command APDU, see CSML 7.2.1.5
+ */
+public class GetDoCommand extends FiRaCommand {
+    @NonNull
+    private final TlvDatum mQueryDataObject;
+
+    private GetDoCommand(@NonNull TlvDatum queryDataObject) {
+        super();
+        mQueryDataObject = queryDataObject;
+    }
+
+    @Override
+    protected byte getCla() {
+        return (byte) 0x00;
+    }
+
+    @Override
+    protected byte getIns() {
+        return (byte) 0xCB;
+    }
+
+    @Override
+    protected byte getP1() {
+        return (byte) 0x3F;
+    }
+
+    @Override
+    protected byte getP2() {
+        return (byte) 0xFF;
+    }
+
+    @Override
+    @NonNull
+    protected StatusWord[] getExpectedSw() {
+        return new StatusWord[] {
+                StatusWord.SW_NO_ERROR,
+                StatusWord.SW_SECURITY_STATUS_NOT_SATISFIED,
+                StatusWord.SW_WRONG_DATA,
+                StatusWord.SW_INCORRECT_P1P2 };
+    }
+
+    @Override
+    @NonNull
+    protected List<TlvDatum> getTlvPayload() {
+        return Arrays.asList(mQueryDataObject);
+    }
+
+    /**
+     * Builds the GetDoCommand.
+     */
+    @NonNull
+    public static GetDoCommand build(@NonNull TlvDatum queryDataObject) {
+        return new GetDoCommand(queryDataObject);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/GetDoResponse.java b/service/java/com/android/server/uwb/secure/csml/GetDoResponse.java
new file mode 100644
index 0000000..14d93dc
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/GetDoResponse.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.csml;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+
+import java.util.Optional;
+
+/**
+ * Response of get Data Object APDU, see CSML 1.0 - 7.2.1.5
+ */
+public class GetDoResponse extends FiRaResponse {
+    @NonNull
+    public final Optional<byte[]> data;
+
+    private GetDoResponse(@NonNull ResponseApdu responseApdu) {
+        super(responseApdu.getStatusWord());
+
+        if (!isSuccess()) {
+            data = Optional.empty();
+            return;
+        }
+        // Don't parse the data. make it opaque data.
+        data = Optional.of(responseApdu.getResponseData());
+    }
+
+    /**
+     * Parse the response of GetDoCommand.
+     */
+    public static GetDoResponse fromResponseApdu(@NonNull ResponseApdu responseApdu) {
+        return new GetDoResponse(responseApdu);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/GetLocalDataCommand.java b/service/java/com/android/server/uwb/secure/csml/GetLocalDataCommand.java
new file mode 100644
index 0000000..bbcd6a0
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/GetLocalDataCommand.java
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Get data command APDU, see CSML 1.0 8.2.2.14.4.1
+ */
+public class GetLocalDataCommand extends FiRaCommand {
+
+    private final byte mP1;
+    private final byte mP2;
+
+    private GetLocalDataCommand(byte p1, byte p2) {
+        super();
+        mP1 = p1;
+        mP2 = p2;
+    }
+
+    /**
+     * Generates the instance of GetLocalDataCommand to get the data of the PA list.
+     */
+    @NonNull
+    public static GetLocalDataCommand getPaListCommand() {
+        return GetLocalDataCommand.build((byte) 0x00, (byte) 0xB0);
+    }
+
+    /**
+     * Generates the instance of GetLocalDataCommand to get the data of
+     * the FiRa applet certificates.
+     */
+    @NonNull
+    public static GetLocalDataCommand getFiRaAppletCertificatesCommand() {
+        return GetLocalDataCommand.build((byte) 0xBF, (byte) 0x21);
+    }
+
+    @Override
+    protected byte getIns() {
+        return (byte) 0xCA;
+    }
+
+    @Override
+    protected byte getP1() {
+        return mP1;
+    }
+
+    @Override
+    protected byte getP2() {
+        return mP2;
+    }
+
+    @Override
+    @NonNull
+    protected StatusWord[] getExpectedSw() {
+        return new StatusWord[] {
+                StatusWord.SW_NO_ERROR,
+                StatusWord.SW_SECURITY_STATUS_NOT_SATISFIED,
+                StatusWord.SW_WRONG_DATA,
+                StatusWord.SW_INCORRECT_P1P2 };
+    }
+
+    @Override
+    @NonNull
+    protected List<TlvDatum> getTlvPayload() {
+        return new ArrayList<TlvDatum>();
+    }
+
+    /**
+     * Builds the GetLocalDataCommand.
+     */
+    @NonNull
+    public static GetLocalDataCommand build(byte p1, byte p2) {
+        return new GetLocalDataCommand(p1, p2);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/GetLocalDataResponse.java b/service/java/com/android/server/uwb/secure/csml/GetLocalDataResponse.java
new file mode 100644
index 0000000..dc3393c
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/GetLocalDataResponse.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.csml;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+
+import java.util.Optional;
+
+/**
+ * Response of get Data command APDU, see CSML 1.0 - 8.2.2.14.4.1
+ */
+public class GetLocalDataResponse extends FiRaResponse {
+    @NonNull
+    public final Optional<byte[]> data;
+
+    private GetLocalDataResponse(@NonNull ResponseApdu responseApdu) {
+        super(responseApdu.getStatusWord());
+
+        if (!isSuccess()) {
+            data = Optional.empty();
+            return;
+        }
+        // Don't parse the data. make it opaque data.
+        data = Optional.of(responseApdu.getResponseData());
+    }
+
+    /**
+     * Parse the response of GetLocalDataCommand.
+     */
+    public static GetLocalDataResponse fromResponseApdu(@NonNull ResponseApdu responseApdu) {
+        return new GetLocalDataResponse(responseApdu);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/InitiateTransactionCommand.java b/service/java/com/android/server/uwb/secure/csml/InitiateTransactionCommand.java
new file mode 100644
index 0000000..b24b555
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/InitiateTransactionCommand.java
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.csml;
+
+import static com.android.server.uwb.util.Constants.UWB_SESSION_TYPE_MULTICAST;
+import static com.android.server.uwb.util.Constants.UWB_SESSION_TYPE_UNICAST;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+import com.android.server.uwb.util.Constants.UwbSessionType;
+import com.android.server.uwb.util.ObjectIdentifier;
+
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Initiate Transaction Command APDU, see CSML 1.0 8.2.2.14.2.8
+ */
+public class InitiateTransactionCommand extends FiRaCommand {
+    private static final Tag UWB_SESSION_ID_TAG = new Tag((byte) 0x80);
+
+    @UwbSessionType
+    private final int mUwbSessionType;
+
+    @NonNull
+    private final Optional<Integer> mUwbSessionId;
+
+    @NonNull
+    private final List<ObjectIdentifier> mAdfOids;
+
+    private InitiateTransactionCommand(@NonNull List<ObjectIdentifier> adfOids,
+            Optional<Integer> uwbSessionId) {
+        super();
+        this.mAdfOids = adfOids;
+        this.mUwbSessionId = uwbSessionId;
+        if (uwbSessionId.isPresent()) {
+            mUwbSessionType = UWB_SESSION_TYPE_MULTICAST;
+        } else {
+            mUwbSessionType = UWB_SESSION_TYPE_UNICAST;
+        }
+    }
+
+    @Override
+    protected byte getIns() {
+        return (byte) 0x12;
+    }
+
+    protected byte getP1() {
+        if (mUwbSessionType == UWB_SESSION_TYPE_UNICAST) {
+            return (byte) 0x00;
+        } else {
+            return (byte) 0x01;
+        }
+    }
+
+    @Override
+    @NonNull
+    protected StatusWord[] getExpectedSw() {
+        return new StatusWord[] {
+                StatusWord.SW_NO_ERROR,
+                StatusWord.SW_CONDITIONS_NOT_SATISFIED,
+                StatusWord.SW_FUNCTION_NOT_SUPPORTED,
+                StatusWord.SW_INCORRECT_P1P2 };
+    }
+    @Override
+    @NonNull
+    protected List<TlvDatum> getTlvPayload() {
+        List<TlvDatum> tlvs = new ArrayList<>();
+        mUwbSessionId.ifPresent(sessionId -> {
+            TlvDatum sessionIdTlv = new TlvDatum(UWB_SESSION_ID_TAG, sessionId);
+            tlvs.add(sessionIdTlv);
+        });
+        for (ObjectIdentifier adfOid : mAdfOids) {
+            tlvs.add(CsmlUtil.encodeObjectIdentifierAsTlv(adfOid));
+        }
+        return tlvs;
+    }
+
+    /**
+     * Build the InitiateTransactionCommand for unicast UWB session.
+     */
+    public static InitiateTransactionCommand buildForUnicast(List<ObjectIdentifier> adfOids) {
+        Preconditions.checkArgument(adfOids.size() > 0);
+        return new InitiateTransactionCommand(adfOids, Optional.empty());
+    }
+
+    /**
+     * Build the InitiateTransactionCommand for multicast UWB session.
+     */
+    @NonNull
+    public static InitiateTransactionCommand buildForMulticast(
+            List<ObjectIdentifier> adfOids, int uwbSessionId) {
+        Preconditions.checkArgument(adfOids.size() > 0);
+        return new InitiateTransactionCommand(adfOids, Optional.of(uwbSessionId));
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/InitiateTransactionResponse.java b/service/java/com/android/server/uwb/secure/csml/InitiateTransactionResponse.java
new file mode 100644
index 0000000..2421c07
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/InitiateTransactionResponse.java
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.csml;
+
+import static com.android.server.uwb.secure.csml.FiRaResponse.PROPRIETARY_RESPONSE_TAG;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+import com.android.server.uwb.secure.iso7816.TlvParser;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Response of Initiate Traction APDU, see CSML 1.0 - 8.2.2.14.2.8
+ */
+public class InitiateTransactionResponse extends FiRaResponse {
+
+    @VisibleForTesting
+    static final Tag STATUS_TAG = new Tag((byte) 0x80);
+    @VisibleForTesting
+    static final Tag DATA_TAG = new Tag((byte) 0x81);
+
+    /**
+     * The data should be sent to the peer device.
+     */
+    @NonNull
+    public final Optional<byte[]> outboundDataToRemoteApplet;
+
+    /**
+     * The status from the response data.
+     */
+    @NonNull
+    private byte mStatus = (byte) 0;
+
+    private boolean hasOutboundData() {
+        return mStatus == (byte) 0x80;
+    }
+
+    private InitiateTransactionResponse(ResponseApdu responseApdu) {
+        super(responseApdu.getStatusWord());
+
+        if (!isSuccess()) {
+            outboundDataToRemoteApplet = Optional.empty();
+            return;
+        }
+        Map<Tag, List<TlvDatum>> tlvsMap = TlvParser.parseTlvs(responseApdu);
+        List<TlvDatum> proprietaryTlv = tlvsMap.get(PROPRIETARY_RESPONSE_TAG);
+        if (proprietaryTlv == null || proprietaryTlv.size() == 0) {
+            outboundDataToRemoteApplet = Optional.empty();
+            return;
+        }
+
+        tlvsMap = TlvParser.parseTlvs(proprietaryTlv.get(0).value);
+        List<TlvDatum> statusTlvs = tlvsMap.get(STATUS_TAG);
+        if (statusTlvs != null && statusTlvs.size() > 0) {
+            mStatus = statusTlvs.get(0).value[0];
+        }
+        if (!hasOutboundData()) {
+            outboundDataToRemoteApplet = Optional.empty();
+            return;
+        }
+        List<TlvDatum> dataTlvs = tlvsMap.get(DATA_TAG);
+        if (dataTlvs != null && dataTlvs.size() > 0) {
+            outboundDataToRemoteApplet = Optional.of(dataTlvs.get(0).value);
+        } else {
+            outboundDataToRemoteApplet = Optional.empty();
+        }
+    }
+
+    /**
+     * Parse the ResponseApdu of InitiateTractionCommand.
+     */
+    @NonNull
+    public static InitiateTransactionResponse fromResponseApdu(
+            @NonNull ResponseApdu responseApdu) {
+        return new InitiateTransactionResponse(responseApdu);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/PutDoCommand.java b/service/java/com/android/server/uwb/secure/csml/PutDoCommand.java
new file mode 100644
index 0000000..fdad21e
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/PutDoCommand.java
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Put data object, see CSML 7.2.1.6
+ */
+public class PutDoCommand extends FiRaCommand {
+
+    // TODO: define a DoTag to convert DO structure.
+    @NonNull
+    private final Tag mDoTag;
+    @NonNull
+    private final byte[] mDoData;
+
+    private PutDoCommand(@NonNull Tag doTag, @NonNull byte[] doData) {
+        super();
+        mDoTag = doTag;
+        mDoData = doData;
+    }
+
+    @Override
+    protected byte getCla() {
+        return (byte) 0x00;
+    }
+
+    @Override
+    protected byte getIns() {
+        return (byte) 0xDB;
+    }
+
+    @Override
+    protected byte getP1() {
+        return (byte) 0x3F;
+    }
+
+    @Override
+    protected byte getP2() {
+        return (byte) 0xFF;
+    }
+
+    @Override
+    @NonNull
+    protected StatusWord[] getExpectedSw() {
+        return new StatusWord[] {
+                StatusWord.SW_NO_ERROR,
+                StatusWord.SW_SECURITY_STATUS_NOT_SATISFIED,
+                StatusWord.SW_WRONG_DATA,
+                StatusWord.SW_NOT_ENOUGH_MEMORY,
+                StatusWord.SW_NC_INCONSISTENT_WITH_TLV,
+                StatusWord.SW_INCORRECT_P1P2 };
+    }
+
+    @Override
+    @NonNull
+    protected List<TlvDatum> getTlvPayload() {
+        return Arrays.asList(new TlvDatum(mDoTag, mDoData));
+    }
+
+    /**
+     * Builds the PutDoCommand.
+     */
+    @NonNull
+    public static PutDoCommand build(@NonNull Tag doTag, @NonNull byte[] doData) {
+        return new PutDoCommand(doTag, doData);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/PutDoResponse.java b/service/java/com/android/server/uwb/secure/csml/PutDoResponse.java
new file mode 100644
index 0000000..bd17af7
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/PutDoResponse.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.csml;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+
+/**
+ * See CSML 1.0 - 7.2.1.6 PUT_DATA Command
+ */
+public class PutDoResponse extends FiRaResponse {
+
+    private PutDoResponse(ResponseApdu responseApdu) {
+        super(responseApdu.getStatusWord());
+    }
+
+    /**
+     * Parse the response of PutDoCommand.
+     */
+    @NonNull
+    public static PutDoResponse fromResponseApdu(@NonNull ResponseApdu responseApdu) {
+        return new PutDoResponse(responseApdu);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/SelectAdfCommand.java b/service/java/com/android/server/uwb/secure/csml/SelectAdfCommand.java
new file mode 100644
index 0000000..aa793ba
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/SelectAdfCommand.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.android.server.uwb.secure.iso7816.Iso7816Constants.P1_SELECT_BY_DEDICATED_FILE_NAME;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.util.ObjectIdentifier;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * SELECT ADF command APDU, see CSML 7.2.1.2
+ * This is sent from the framework to FiRa applet locally, ignore the privacy protection bit.
+ */
+public class SelectAdfCommand extends FiRaCommand {
+    private final ObjectIdentifier mAdfOid;
+
+    private SelectAdfCommand(@NonNull ObjectIdentifier adfOid) {
+        super();
+        mAdfOid = adfOid;
+    }
+
+    @Override
+    protected byte getIns() {
+        return (byte) 0xA5;
+    }
+
+    @Override
+    protected byte getP1() {
+        return P1_SELECT_BY_DEDICATED_FILE_NAME;
+    }
+
+    @Override
+    @NonNull
+    protected StatusWord[] getExpectedSw() {
+        return new StatusWord[] {
+                StatusWord.SW_NO_ERROR,
+                StatusWord.SW_APPLET_SELECT_FAILED,
+                StatusWord.SW_FILE_NOT_FOUND,
+                StatusWord.SW_INCORRECT_P1P2 };
+    }
+
+    @Override
+    @NonNull
+    protected List<TlvDatum> getTlvPayload() {
+        return Arrays.asList(CsmlUtil.encodeObjectIdentifierAsTlv(mAdfOid));
+    }
+
+    /**
+     * Builds the SelectAdfCommand.
+     */
+    @NonNull
+    public static SelectAdfCommand build(ObjectIdentifier adfOid) {
+        return new SelectAdfCommand(adfOid);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/SelectAdfResponse.java b/service/java/com/android/server/uwb/secure/csml/SelectAdfResponse.java
new file mode 100644
index 0000000..39e28ee
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/SelectAdfResponse.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.csml;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+
+/**
+ * See CSML 1.0 - 7.2.1.2, SELECT ADF Response
+ * Ignores the data field from local SELECT ADF Rsponse.
+ */
+public class SelectAdfResponse extends FiRaResponse {
+
+    private SelectAdfResponse(ResponseApdu responseApdu) {
+        super(responseApdu.getStatusWord());
+    }
+
+    /**
+     * Parse the response of SelectAdfCommand.
+     */
+    @NonNull
+    public static SelectAdfResponse fromResponseApdu(@NonNull ResponseApdu responseApdu) {
+        return new SelectAdfResponse(responseApdu);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/SwapInAdfCommand.java b/service/java/com/android/server/uwb/secure/csml/SwapInAdfCommand.java
new file mode 100644
index 0000000..ef00909
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/SwapInAdfCommand.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Swap in the secure blob for imported ADF used by dynamic STS. static STS is not supported.
+ */
+public class SwapInAdfCommand extends FiRaCommand {
+    private static final Tag SECURE_BLOB_TAG = new Tag((byte) 0xDF, (byte) 0x51);
+
+    // the secure blob should have OID and its ADF contents.
+    @NonNull
+    private final byte[] mSecureBlob;
+
+    private SwapInAdfCommand(@NonNull byte[] secureBlob) {
+        super();
+        mSecureBlob = secureBlob;
+    }
+
+    @Override
+    protected byte getIns() {
+        return (byte) 0x40;
+    }
+
+    @Override
+    protected byte getP1() {
+        // acquire slot
+        return (byte) 0x00;
+    }
+
+    @Override
+    @NonNull
+    protected StatusWord[] getExpectedSw() {
+        return new StatusWord[] {
+                StatusWord.SW_NO_ERROR,
+                StatusWord.SW_WRONG_LENGTH,
+                StatusWord.SW_CONDITIONS_NOT_SATISFIED,
+                StatusWord.SW_FILE_NOT_FOUND,
+                StatusWord.SW_NOT_ENOUGH_MEMORY,
+                StatusWord.SW_INCORRECT_P1P2 };
+    }
+
+    @Override
+    @NonNull
+    protected List<TlvDatum> getTlvPayload() {
+        return Arrays.asList(new TlvDatum(SECURE_BLOB_TAG, mSecureBlob));
+    }
+
+    /**
+     * Builds the SwapInAdfCommand.
+     */
+    @NonNull
+    public static SwapInAdfCommand build(@NonNull byte[] secureBlob) {
+        return new SwapInAdfCommand(secureBlob);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/SwapInAdfResponse.java b/service/java/com/android/server/uwb/secure/csml/SwapInAdfResponse.java
new file mode 100644
index 0000000..18b996c
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/SwapInAdfResponse.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.csml;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+import com.android.server.uwb.secure.iso7816.TlvParser;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * See CSML 1.0 - 8.2.2.14.1.5 SWAP ADF
+ */
+public class SwapInAdfResponse extends FiRaResponse {
+    //TODO: the tag is not defined in CSML, use 0x06.
+    @VisibleForTesting
+    static final Tag SLOT_IDENTIFIER_TAG = new Tag((byte) 0x06);
+
+    @NonNull
+    public final Optional<byte[]> slotIdentifier;
+
+    private SwapInAdfResponse(ResponseApdu responseApdu) {
+        super(responseApdu.getStatusWord());
+
+        if (!isSuccess()) {
+            slotIdentifier = Optional.empty();
+            return;
+        }
+
+        Map<Tag, List<TlvDatum>> tlvsMap = TlvParser.parseTlvs(responseApdu);
+        List<TlvDatum> tlvs = tlvsMap.get(SLOT_IDENTIFIER_TAG);
+        if (tlvs != null && tlvs.size() > 0) {
+            slotIdentifier = Optional.of(tlvs.get(0).value);
+        } else {
+            slotIdentifier = Optional.empty();
+        }
+    }
+
+    /**
+     * Parse the response of SwapInCommand.
+     */
+    public static SwapInAdfResponse fromResponseApdu(@NonNull ResponseApdu responseApdu) {
+        return new SwapInAdfResponse(responseApdu);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/SwapOutAdfCommand.java b/service/java/com/android/server/uwb/secure/csml/SwapOutAdfCommand.java
new file mode 100644
index 0000000..3d1b200
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/SwapOutAdfCommand.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Swap in the secure blob for imported ADF used by dynamic STS. static STS is not supported.
+ */
+public class SwapOutAdfCommand extends FiRaCommand {
+    // TODO: undefined in CSML, use 0x06 as temp tag
+    private static final Tag SLOT_IDENTIFIER_TAG = new Tag((byte) 0x06);
+
+    // use byte[] as the slot identifier is variable per implementation.
+    @NonNull
+    private final byte[] mSlotIdentifier;
+
+    private SwapOutAdfCommand(@NonNull byte[] slotIdentifier) {
+        super();
+        mSlotIdentifier = slotIdentifier;
+    }
+
+    @Override
+    protected byte getIns() {
+        return (byte) 0x40;
+    }
+
+    @Override
+    protected byte getP1() {
+        // release slot
+        return (byte) 0x01;
+    }
+
+    @Override
+    @NonNull
+    protected StatusWord[] getExpectedSw() {
+        return new StatusWord[] {
+                StatusWord.SW_NO_ERROR,
+                StatusWord.SW_WRONG_LENGTH,
+                StatusWord.SW_CONDITIONS_NOT_SATISFIED,
+                StatusWord.SW_FILE_NOT_FOUND,
+                StatusWord.SW_NOT_ENOUGH_MEMORY,
+                StatusWord.SW_INCORRECT_P1P2 };
+    }
+
+    @Override
+    @NonNull
+    protected List<TlvDatum> getTlvPayload() {
+        return Arrays.asList(new TlvDatum(SLOT_IDENTIFIER_TAG, mSlotIdentifier));
+    }
+
+    /**
+     * Builds the SwapOutAdfCommand.
+     */
+    @NonNull
+    public static SwapOutAdfCommand build(@NonNull byte[] slotIdentifier) {
+        return new SwapOutAdfCommand(slotIdentifier);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/SwapOutAdfResponse.java b/service/java/com/android/server/uwb/secure/csml/SwapOutAdfResponse.java
new file mode 100644
index 0000000..73d2ad8
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/SwapOutAdfResponse.java
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.csml;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+
+/**
+ * See CSML 1.0 - 8.2.2.14.1.5 SWAP ADF
+ */
+public class SwapOutAdfResponse extends FiRaResponse {
+
+    private SwapOutAdfResponse(@NonNull ResponseApdu responseApdu) {
+        super(responseApdu.getStatusWord());
+    }
+
+    /**
+     * Parse the response of SwapOutCommand.
+     */
+    public static SwapOutAdfResponse fromResponseApdu(@NonNull ResponseApdu responseApdu) {
+        return new SwapOutAdfResponse(responseApdu);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/TunnelCommand.java b/service/java/com/android/server/uwb/secure/csml/TunnelCommand.java
new file mode 100644
index 0000000..707c316
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/TunnelCommand.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import android.annotation.NonNull;
+
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tunnel command APDU, see CSML 8.2.2.14.2.7.
+ * Command is used to send the payload to remote device through the secure channel.
+ */
+public class TunnelCommand extends FiRaCommand {
+    private static final Tag TUNNEL_DATA_TAG = new Tag((byte) 0x81);
+
+    @NonNull
+    private final byte[] mTunnelData;
+
+    private TunnelCommand(@NonNull byte[] tunnelData) {
+        super();
+        mTunnelData = tunnelData;
+    }
+
+    @Override
+    protected byte getIns() {
+        return (byte) 0x14;
+    }
+
+    @Override
+    @NonNull
+    protected StatusWord[] getExpectedSw() {
+        return new StatusWord[] {
+                StatusWord.SW_NO_ERROR,
+                StatusWord.SW_CONDITIONS_NOT_SATISFIED,
+                StatusWord.SW_FUNCTION_NOT_SUPPORTED,
+                StatusWord.SW_INCORRECT_P1P2 };
+    }
+
+    @Override
+    @NonNull
+    protected List<TlvDatum> getTlvPayload() {
+        return Arrays.asList(
+                new TlvDatum(FIRA_PROPRIETARY_COMMAND_TEMP_TAG,
+                        new TlvDatum(TUNNEL_DATA_TAG, mTunnelData)));
+    }
+
+    /**
+     * Builds the TunnelCommand.
+     */
+    @NonNull
+    public static TunnelCommand build(@NonNull byte[] tunnelData) {
+        return new TunnelCommand(tunnelData);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/csml/TunnelResponse.java b/service/java/com/android/server/uwb/secure/csml/TunnelResponse.java
new file mode 100644
index 0000000..51c5fd5
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/csml/TunnelResponse.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.csml;
+
+import static com.android.server.uwb.secure.csml.FiRaResponse.PROPRIETARY_RESPONSE_TAG;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+import com.android.server.uwb.secure.iso7816.TlvParser;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * See CSML 1.0 - 8.2.2.14.2.7 Tunnel
+ */
+public class TunnelResponse extends FiRaResponse {
+    @VisibleForTesting
+    static final Tag DATA_TAG = new Tag(new byte[] { (byte) 0x81 });
+
+    /**
+     * The data should be sent to the peer device.
+     */
+    @NonNull
+    public final Optional<byte[]> outboundDataOrApdu;
+
+    private TunnelResponse(ResponseApdu responseApdu) {
+        super(responseApdu.getStatusWord());
+        if (isSuccess()) {
+            Map<Tag, List<TlvDatum>> tlvsMap = TlvParser.parseTlvs(responseApdu);
+            List<TlvDatum> proprietaryTlv = tlvsMap.get(PROPRIETARY_RESPONSE_TAG);
+            if (proprietaryTlv == null || proprietaryTlv.size() == 0) {
+                outboundDataOrApdu = Optional.empty();
+                return;
+            }
+
+            tlvsMap = TlvParser.parseTlvs(proprietaryTlv.get(0).value);
+            List<TlvDatum> dataTlvs = tlvsMap.get(DATA_TAG);
+            if (dataTlvs != null && dataTlvs.size() > 0) {
+                outboundDataOrApdu = Optional.of(dataTlvs.get(0).value);
+            } else {
+                outboundDataOrApdu = Optional.empty();
+            }
+        } else {
+            outboundDataOrApdu = Optional.empty();
+        }
+    }
+
+    /**
+     * Parse the response of InitiateTractionCommand.
+     */
+    public static TunnelResponse fromResponseApdu(@NonNull ResponseApdu responseApdu) {
+        return new TunnelResponse(responseApdu);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/iso7816/CommandApdu.java b/service/java/com/android/server/uwb/secure/iso7816/CommandApdu.java
new file mode 100644
index 0000000..094d103
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/iso7816/CommandApdu.java
@@ -0,0 +1,440 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.iso7816;
+
+import static com.android.server.uwb.secure.iso7816.Iso7816Constants.OFFSET_CLA;
+import static com.android.server.uwb.secure.iso7816.Iso7816Constants.OFFSET_INS;
+import static com.android.server.uwb.secure.iso7816.Iso7816Constants.OFFSET_LC;
+import static com.android.server.uwb.secure.iso7816.Iso7816Constants.OFFSET_P1;
+import static com.android.server.uwb.secure.iso7816.Iso7816Constants.OFFSET_P2;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.uwb.util.Hex;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.primitives.UnsignedBytes;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Collection;
+
+import javax.annotation.Nullable;
+
+/**
+ * Representation of an ISO7816-4 APDU command. Standard and extended length APDUs are supported.
+ * For standard APDUs, the maximum command length is 255 bytes and the maximum response length is
+ * 256 bytes. For extended APDUs, the maximum command length is 65535 bytes and the maximum response
+ * length is 65536 bytes.
+ */
+public class CommandApdu {
+
+    /** Sets the Le byte to return as many bytes as possible (all remaining) up to 256. */
+    public static final int LE_ANY = 0;
+
+    private static final int NO_EXPECTED_RESPONSE = -1;
+
+    private static final int MAX_CDATA_LEN = 255; // As per ISO7816-4 for standard length APDU's.
+    private static final int MAX_RDATA_LEN = 256; // Maximum standard length response.
+
+    // ISO7816-4 APDU components.
+    private final byte mCla; // Class byte.
+
+    private final byte mIns; // Instruction byte.
+
+    private final byte mP1; // Parameter 1.
+
+    private final byte mP2; // Parameter 2.
+
+    private final int mLc; // Length of command data.
+
+    private final int mLe; // Expected length of response.
+
+    private final boolean mForceExtended;
+
+    // cdata is always cloned (an alternative immutable class would cost in performance)
+    private final byte[] mCdata; // Command data.
+
+    private final ImmutableSet<StatusWord> mExpected;
+
+    /**
+     * Constructs a case 4 APDU (Command with data and an expected response).
+     *
+     * <p>When the {@link CommandApdu} instance is created, only the low order byte is used for the
+     * values of cla, ins, p1 and p2. They are int's in the constructor as a convenience only so as
+     * to ensure the caller does not need to cast to a byte. The casting and masking is handled
+     * internally.
+     *
+     * <p>The command data in an {@link CommandApdu} instance is immutable. The data passed in will
+     * be copied and updates to the original data will not be reflected in the {@link CommandApdu}
+     * instance.
+     */
+    @VisibleForTesting
+    CommandApdu(
+            int cla,
+            int ins,
+            int p1,
+            int p2,
+            @Nullable byte[] cdata,
+            int le,
+            boolean forceExtended,
+            StatusWord... exp) {
+        Preconditions.checkArgument(exp.length > 0);
+
+        this.mCla = (byte) (cla & 0xff);
+        this.mIns = (byte) (ins & 0xff);
+        this.mP1 = (byte) (p1 & 0xff);
+        this.mP2 = (byte) (p2 & 0xff);
+        this.mCdata = (cdata != null) ? cdata.clone() : new byte[0];
+        this.mLc = this.mCdata.length;
+        this.mLe = le;
+        this.mForceExtended = forceExtended;
+
+        Preconditions.checkArgument((mLc >> Short.SIZE) == 0, "Lc must be between 0 and 65,535: %s",
+                mLc);
+        Preconditions.checkArgument(
+                le == NO_EXPECTED_RESPONSE || (le >> Short.SIZE) == 0,
+                "Le must be between 0 and 65,535: %s",
+                le);
+
+        mExpected = ImmutableSet.copyOf(exp);
+        // for now, don't allow any unlisted status words to be set as expected
+        Preconditions.checkArgument(StatusWord.areAllKnown(mExpected));
+    }
+
+    /**
+     * Gets the encoded byte stream that represents this APDU.
+     *
+     * <p>The encoded form is: <code>cla |
+     * ins | p1 | p2 | lc | data | le</code>
+     */
+    public byte[] getEncoded() {
+        // Minimum APDU length (case 1 APDU's).
+        int len = 4;
+        boolean extended = mForceExtended;
+
+        // Adjust length for any command data.
+        if (mLc > 0) {
+            // Add the data length plus make space for Lc.
+            len += 1 + mLc;
+
+            if (mLc > MAX_CDATA_LEN) {
+                len += 2; // Add room for an extended length APDU.
+                extended = true;
+            }
+        } else {
+            if (mLe > MAX_RDATA_LEN) {
+                extended = true;
+            }
+        }
+
+        // Adjust for Le if present.
+        if (mLe > NO_EXPECTED_RESPONSE) {
+            len++;
+
+            // Check if we need to make Le extended as well.
+            if (extended) {
+                len += 2;
+            }
+        }
+
+        // Create the APDU header.
+        byte[] apdu = new byte[len];
+        apdu[OFFSET_CLA] = mCla;
+        apdu[OFFSET_INS] = mIns;
+        apdu[OFFSET_P1] = mP1;
+        apdu[OFFSET_P2] = mP2;
+
+        int off = OFFSET_LC;
+
+        // Check to see if data needs to be added to the command.
+        if (mLc > 0) {
+            // Only add Lc if there is data.
+            if (extended) {
+                apdu[off++] = 0;
+                apdu[off++] = (byte) (mLc >> 8);
+                apdu[off++] = (byte) (mLc & 0xff);
+                System.arraycopy(mCdata, 0, apdu, off, mLc);
+                off += mLc;
+            } else {
+                apdu[off++] = (byte) mLc;
+                System.arraycopy(mCdata, 0, apdu, off, mLc);
+                off += mLc;
+            }
+        }
+
+        if (mLe > NO_EXPECTED_RESPONSE) {
+            if (extended) {
+                apdu[off++] = 0;
+                apdu[off++] = (byte) (mLe >> 8);
+                apdu[off++] = (byte) (mLe & 0xff);
+            } else {
+                // When the length is exactly 256, the value is cast to 0x00.
+                // A command expecting no data does not send an Le.
+                apdu[off] = (byte) mLe;
+            }
+        }
+
+        return apdu;
+    }
+
+    /**
+     * Gets the CLA of APDU.
+     */
+    public byte getCla() {
+        return mCla;
+    }
+
+    /**
+     * Gets the INS of APDU.
+     */
+    public byte getIns() {
+        return mIns;
+    }
+
+    /**
+     * Gets the P1 of APDU.
+     */
+    public byte getP1() {
+        return mP1;
+    }
+
+    /**
+     * Gets the P2 of APDU.
+     */
+    public byte getP2() {
+        return mP2;
+    }
+
+    /** Returns true if this commands expects data back from the card. i.e. If le is >= 0. */
+    public boolean hasLe() {
+        return mLe != NO_EXPECTED_RESPONSE;
+    }
+
+    /**
+     * Gets the LE of APDU.
+     */
+    public int getLe() {
+        Preconditions.checkState(hasLe());
+        return mLe;
+    }
+
+    /**
+     * Returns a copy of the command data for the APDU. Updates to this copy will not affect the
+     * internal copy in this instance.
+     */
+    public byte[] getCommandData() {
+        return mCdata.clone();
+    }
+
+    /**
+     * Returns the expected {@link StatusWord} responses for this {@link CommandApdu}. Any {@link
+     * StatusWord} that is expected will not cause an exception to be thrown.
+     */
+    public ImmutableSet<StatusWord> getExpected() {
+        return mExpected;
+    }
+
+    /**
+     * Check if the given status word is accepted.
+     */
+    public boolean acceptsStatusWord(StatusWord actual) {
+        return mExpected.contains(actual);
+    }
+
+    /**
+     * check if the status word of the given ResponseApdu is accepted.
+     */
+    public boolean acceptsResponse(ResponseApdu response) {
+        return acceptsStatusWord(StatusWord.fromInt(response.getStatusWord()));
+    }
+
+    @Override
+    public String toString() {
+        StringWriter stringWriter = new StringWriter();
+        PrintWriter printWriter = new PrintWriter(stringWriter);
+
+        printWriter.printf("Command : CLA=%02x, INS=%02x, P1=%02x, P2=%02x", mCla, mIns, mP1, mP2);
+
+        if (mLc > 0) {
+            printWriter.printf(", Lc=%04x [%s]", mLc, Hex.encode(mCdata));
+        }
+
+        if (mLe > NO_EXPECTED_RESPONSE) {
+            printWriter.printf(", Le=%04x", mLe);
+        }
+
+        return stringWriter.toString();
+    }
+
+    /**
+     * Parses a command APDU and returns an {@link CommandApdu} instance. Currently only supports
+     * standard length APDU's.
+     */
+    public static CommandApdu parse(byte[] command) {
+        ByteBuffer buf = ByteBuffer.wrap(Preconditions.checkNotNull(command));
+        byte cla = buf.get();
+        byte ins = buf.get();
+        byte p1 = buf.get();
+        byte p2 = buf.get();
+
+        Builder builder = builder(cla, ins, p1, p2);
+
+        if (buf.hasRemaining()) {
+            int lc = UnsignedBytes.toInt(buf.get());
+
+            if (!buf.hasRemaining()) {
+                builder.setLe(lc);
+            } else {
+                byte[] cdata = new byte[lc];
+                buf.get(cdata);
+                builder.setCdata(cdata);
+
+                if (buf.hasRemaining()) {
+                    builder.setLe(UnsignedBytes.toInt(buf.get()));
+                }
+            }
+        }
+
+        if (buf.hasRemaining()) {
+            throw new IllegalArgumentException("Invalid APDU: " + Hex.encode(command));
+        }
+
+        return builder.build();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj == null) {
+            return false;
+        }
+
+        if (this.getClass() == obj.getClass()) {
+            CommandApdu other = (CommandApdu) obj;
+            // @formatter:off
+            return this.mCla == other.mCla
+                    && this.mIns == other.mIns
+                    && this.mP1 == other.mP1
+                    && this.mP2 == other.mP2
+                    && Arrays.equals(this.mCdata, other.mCdata)
+                    && this.mLe == other.mLe
+                    && this.mExpected.equals(other.mExpected);
+            // @formatter:on
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mCla, mIns, mP1, mP2, Arrays.hashCode(mCdata), mLe, mExpected);
+    }
+
+    /**
+     * Help method to get the Builder of the CommandApdu.
+     */
+    public static Builder builder(int cla, int ins, int p1, int p2) {
+        return new Builder(cla, ins, p1, p2);
+    }
+
+    /** Builder for {@link CommandApdu} instances. */
+    public static class Builder {
+
+        // ISO7816-4 APDU components.
+        private final byte mCla; // Class byte.
+
+        private final byte mIns; // Instruction byte.
+
+        private final byte mP1; // Parameter 1.
+
+        private final byte mP2; // Parameter 2.
+
+        private int mLe = NO_EXPECTED_RESPONSE; // Expected length of response.
+
+        private byte[] mCdata = {}; // Command data.
+
+        @Nullable private StatusWord[] mExpected = null;
+
+        private boolean mForceExtended = false;
+
+        private Builder(int cla, int ins, int p1, int p2) {
+            this.mCla = (byte) cla;
+            this.mIns = (byte) ins;
+            this.mP1 = (byte) p1;
+            this.mP2 = (byte) p2;
+        }
+
+        /**
+         * Sets the LE of the CommandApdu.
+         */
+        public Builder setLe(int le) {
+            this.mLe = le;
+            return this;
+        }
+
+        /**
+         * Sets the data field of the CommandApdu.
+         */
+        public Builder setCdata(byte[] cdata) {
+            this.mCdata = cdata;
+            return this;
+        }
+
+        /**
+         * Sets the expected status words of the response for the CommandApdu.
+         * Slightly less efficient helper method that makes going from an instance
+         * to a builder easier.
+         */
+        public Builder setExpected(Collection<StatusWord> expected) {
+            return setExpected(expected.toArray(new StatusWord[expected.size()]));
+        }
+
+        /**
+         * Sets the expected status words of the response for the CommandApdu.
+         */
+        public Builder setExpected(StatusWord... expected) {
+            Preconditions.checkArgument(expected.length > 0);
+            this.mExpected = expected;
+            return this;
+        }
+
+        /**
+         * Sets the extended length bit of the CommandApdu.
+         */
+        public Builder setExtendedLength() {
+            mForceExtended = true;
+            return this;
+        }
+
+        /**
+         * Builds the instance of CommandApdu.
+         */
+        public CommandApdu build() {
+            return new CommandApdu(
+                    mCla,
+                    mIns,
+                    mP1,
+                    mP2,
+                    mCdata,
+                    mLe,
+                    mForceExtended,
+                    mExpected != null ? mExpected : new StatusWord[] {StatusWord.SW_NO_ERROR});
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/iso7816/Iso7816Constants.java b/service/java/com/android/server/uwb/secure/iso7816/Iso7816Constants.java
new file mode 100644
index 0000000..5a8ae3f
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/iso7816/Iso7816Constants.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.iso7816;
+
+/** A sampling of constants defined by ISO7816. */
+public abstract class Iso7816Constants {
+    public static final byte CLA_BASE = 0x00;
+
+    public static final byte CLA_PROPRIETARY = (byte) 0x80;
+
+    // ISO7816-4 CLA mask indicating that command chaining is being used
+    public static final byte CLA_COMMAND_CHAINING_MASK = (byte) 0x10;
+
+    public static final byte INS_SELECT = (byte) 0xA4;
+
+    public static final byte INS_READ_RECORD = (byte) 0xB2;
+
+    public static final byte INS_GET_DATA = (byte) 0xCA;
+
+    public static final byte INS_GET_PROCSESSING_OPTIONS = (byte) 0xA8;
+
+    public static final byte OFFSET_CLA = 0;
+
+    public static final byte OFFSET_INS = 1;
+
+    public static final byte OFFSET_P1 = 2;
+
+    public static final byte OFFSET_P2 = 3;
+
+    public static final byte OFFSET_LC = 4;
+
+    public static final byte OFFSET_CDATA = 5;
+
+    /** Used with {@link #INS_SELECT} to select an application by application DF (aka AID). */
+    public static final byte P1_SELECT_BY_DEDICATED_FILE_NAME = (byte) 0x04;
+
+    public static final byte TAG_LIST = (byte) 0x5C;
+
+    public static final byte EXTENDED_HEAD_LIST = (byte) 0x4D;
+
+    private Iso7816Constants() {
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/iso7816/ResponseApdu.java b/service/java/com/android/server/uwb/secure/iso7816/ResponseApdu.java
new file mode 100644
index 0000000..21f66e6
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/iso7816/ResponseApdu.java
@@ -0,0 +1,250 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.iso7816;
+
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_APPLET_SELECT_FAILED;
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_CLA_NOT_SUPPORTED;
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_CONDITIONS_NOT_SATISFIED;
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_FILE_NOT_FOUND;
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_INCORRECT_P1P2;
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_INS_NOT_SUPPORTED;
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_NO_ERROR;
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_UNKNOWN_ERROR;
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_WRONG_DATA;
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_WRONG_LE;
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_WRONG_LENGTH;
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_WRONG_P1P2;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.uwb.util.Hex;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.primitives.Bytes;
+import com.google.common.primitives.Shorts;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+
+/** A class that represents the data contained in an ISO/IEC 7816-4 Response APDU. */
+public class ResponseApdu {
+
+    public static final ResponseApdu SW_CONDITIONS_NOT_SATISFIED_APDU =
+            ResponseApdu.fromStatusWord(SW_CONDITIONS_NOT_SATISFIED);
+
+    public static final ResponseApdu SW_INCORRECT_P1P2_APDU =
+            ResponseApdu.fromStatusWord(SW_INCORRECT_P1P2);
+
+    public static final ResponseApdu SW_FILE_NOT_FOUND_APDU =
+            ResponseApdu.fromStatusWord(SW_FILE_NOT_FOUND);
+
+    public static final ResponseApdu SW_WRONG_P1P2_APDU =
+            ResponseApdu.fromStatusWord(SW_WRONG_P1P2);
+
+    public static final ResponseApdu SW_WRONG_LE_APDU =
+            ResponseApdu.fromStatusWord(SW_WRONG_LE);
+
+    public static final ResponseApdu SW_WRONG_DATA_APDU =
+            ResponseApdu.fromStatusWord(SW_WRONG_DATA);
+
+    public static final ResponseApdu SW_WRONG_LENGTH_APDU =
+            ResponseApdu.fromStatusWord(SW_WRONG_LENGTH);
+
+    public static final ResponseApdu SW_CLA_NOT_SUPPORTED_APDU =
+            ResponseApdu.fromStatusWord(SW_CLA_NOT_SUPPORTED);
+
+    public static final ResponseApdu SW_INS_NOT_SUPPORTED_APDU =
+            ResponseApdu.fromStatusWord(SW_INS_NOT_SUPPORTED);
+
+    public static final ResponseApdu SW_WRONG_FILE_APDU =
+            ResponseApdu.fromStatusWord(SW_FILE_NOT_FOUND);
+
+    public static final ResponseApdu SW_UNKNOWN_APDU = ResponseApdu.fromStatusWord(
+            SW_UNKNOWN_ERROR);
+
+    public static final ResponseApdu SW_SUCCESS_APDU = ResponseApdu.fromStatusWord(SW_NO_ERROR);
+
+    public static final ResponseApdu SW_APPLET_SELECT_FAILED_APDU =
+            ResponseApdu.fromStatusWord(SW_APPLET_SELECT_FAILED);
+
+    private static final long NO_TIME_RECORDED = -1L;
+
+    private static final int SIZE_OF_SW = 2;
+
+    private static final int MASK_OF_SW = 0xffff;
+
+    private final byte[] mRdata;
+
+    private final int mSw;
+
+    private final long mCmdTimeMillis;
+
+    @VisibleForTesting
+    ResponseApdu(byte[] rdata, int sw, long cmdTimeMillis) {
+        this.mRdata = rdata;
+        this.mSw = sw;
+        this.mCmdTimeMillis = cmdTimeMillis;
+    }
+
+    /**
+     * Parses a raw APDU response to set the response data and status word. A response consists of
+     * at
+     * least a two byte status word and any number of data bytes. A standard length APDU supports
+     * 256
+     * bytes of data and 2 bytes of status word while and extended length APDU supports 32KB of
+     * response data. A minimum response is simply a status word.
+     *
+     * @param response The raw response from the card to parse.
+     * @throws IllegalArgumentException if the response is less than 2 bytes long.
+     */
+    public static ResponseApdu fromResponse(byte[] response) {
+        return fromResponse(response, NO_TIME_RECORDED, TimeUnit.MILLISECONDS);
+    }
+
+  /**
+   * Generate the ResponseApdu from the byte array(data) and status word.
+   */
+    public static ResponseApdu fromDataAndStatusWord(byte[] data, int sw) {
+        Preconditions.checkArgument((sw >> Short.SIZE) == 0);
+        return fromResponse(
+                Bytes.concat(data == null ? new byte[]{} : data, Shorts.toByteArray((short) sw)));
+    }
+
+  /**
+   * Generate the ResponseApdu form the list of TlvDatum and status word.
+   */
+    public static ResponseApdu fromDataAndStatusWord(List<TlvDatum> data, int sw) {
+        byte[] dataBytes = new byte[]{};
+        for (TlvDatum tlvDatum : data) {
+            dataBytes = Bytes.concat(dataBytes, tlvDatum.toBytes());
+        }
+        return fromDataAndStatusWord(dataBytes, sw);
+    }
+
+  /**
+   * Generate the ResponseApdu form the status word.
+   */
+    public static ResponseApdu fromStatusWord(StatusWord sw) {
+        return fromResponse(sw.toBytes());
+    }
+
+    /**
+     * Parses a raw APDU response to set the response data and status word. A response consists of
+     * at
+     * least a two byte status word and any number of data bytes. A standard length APDU supports
+     * 256
+     * bytes of data and 2 bytes of status word while and extended length APDU supports 32KB of
+     * response data. A minimum response is simply a status word.
+     *
+     * @param response The raw response from the card to parse.
+     * @param time     the time for the command to execute.
+     * @param timeUnit the {@link TimeUnit} of the execution time.
+     * @throws IllegalArgumentException if the response is less than 2 bytes long.
+     */
+    public static ResponseApdu fromResponse(byte[] response, long time, TimeUnit timeUnit) {
+        Preconditions.checkNotNull(response);
+        int len = response.length;
+        long cmdTimeMillis = timeUnit.toMillis(time);
+
+        // A response must at least have a status word (2 bytes).
+        Preconditions.checkArgument(
+                len >= SIZE_OF_SW,
+                "Invalid response APDU after %sms. Must be at least 2 bytes long: [%s]",
+                cmdTimeMillis,
+                Hex.encode(response));
+
+        ByteBuffer buffer = ByteBuffer.wrap(response);
+
+        // Extract and store any response data.
+        int rdataLen = len - SIZE_OF_SW;
+        byte[] rdata = new byte[rdataLen];
+        buffer.get(rdata, 0, rdataLen);
+
+        // Extract and set the status word.
+        int sw = buffer.getShort() & MASK_OF_SW;
+
+        return new ResponseApdu(rdata, sw, cmdTimeMillis);
+    }
+
+    /**
+     * Returns a copy of the response data for the APDU. Updates to this copy will not affect the
+     * internal copy in this instance.
+     */
+    public byte[] getResponseData() {
+        return mRdata.clone();
+    }
+
+    /**
+     * Gets the status word.
+     */
+    public int getStatusWord() {
+        return mSw;
+    }
+
+    /**
+     * Convert the ResponseApdu to the byte array.
+     */
+    public byte[] toByteArray() {
+        return Bytes.concat(mRdata, Shorts.toByteArray((short) mSw));
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder("Response: ");
+
+        if (mRdata != null && mRdata.length > 0) {
+            sb.append(Hex.encode(mRdata)).append(", ");
+        }
+
+        sb.append(String.format("SW=%04x", mSw));
+
+        if (mCmdTimeMillis > NO_TIME_RECORDED) {
+            sb.append(String.format(Locale.US, ", elapsed: %dms", mCmdTimeMillis));
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>This ignores the time the APDU took to complete and only compares the response data and
+     * status word.
+     */
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj == null) {
+            return false;
+        }
+
+        if (this.getClass() == obj.getClass()) {
+            ResponseApdu other = (ResponseApdu) obj;
+            return Arrays.equals(this.mRdata, other.mRdata) && this.mSw == other.mSw;
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(Arrays.hashCode(mRdata), mSw);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/iso7816/StatusWord.java b/service/java/com/android/server/uwb/secure/iso7816/StatusWord.java
new file mode 100644
index 0000000..64522ff
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/iso7816/StatusWord.java
@@ -0,0 +1,254 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.iso7816;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.primitives.Shorts;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** Representation of some common ISO7816-4 and GlobalPlatform status words. */
+public final class StatusWord {
+
+    public static final StatusWord SW_NO_ERROR =
+            new StatusWord(0x9000, "no error");
+
+    public static final StatusWord SW_RESPONSE_BYTES_STILL_AVAILABLE =
+            new StatusWord(0x6100, "Response bytes still available");
+
+    public static final StatusWord SW_WARNING_STATE_UNCHANGED =
+            new StatusWord(0x6200, "Warning: State unchanged");
+
+    public static final StatusWord SW_CARD_MANAGER_LOCKED =
+            new StatusWord(0x6283, "Warning: Card Manager is locked");
+
+    public static final StatusWord SW_WARNING_NO_INFO_GIVEN =
+            new StatusWord(0x6300, "Warning: State changed (no information given)");
+
+    public static final StatusWord SW_WARNING_MORE_DATA =
+            new StatusWord(0x6310, "more data");
+
+    public static final StatusWord SW_VERIFY_FAILED =
+            new StatusWord(0x63C0, "PIN authentication failed.");
+
+    public static final StatusWord SW_NO_SPECIFIC_DIAGNOSTIC =
+            new StatusWord(0x6400, "No specific diagnostic");
+
+    public static final StatusWord SW_REQUESTED_ELEMENTS_NOT_AVAILABLE =
+            new StatusWord(0x6402, "Requested elements not available");
+
+    public static final StatusWord SW_ICA_ALREADY_EXISTS =
+            new StatusWord(0x6409, "ICA Already Exists");
+
+    public static final StatusWord SW_WRONG_LENGTH =
+            new StatusWord(0x6700, "Wrong length");
+
+    public static final StatusWord SW_SECURITY_STATUS_NOT_SATISFIED =
+            new StatusWord(0x6982, "Security status not satisfied");
+
+    public static final StatusWord SW_FILE_INVALID =
+            new StatusWord(0x6983, "File invalid");
+
+    public static final StatusWord SW_REFERENCE_DATA_NOT_USABLE =
+            new StatusWord(0x6984, "Reference data not usable");
+
+    public static final StatusWord SW_CONDITIONS_NOT_SATISFIED =
+            new StatusWord(0x6985, "Conditions of use not satisfied");
+
+    public static final StatusWord SW_COMMAND_NOT_ALLOWED =
+            new StatusWord(0x6986, "Command not allowed");
+
+    public static final StatusWord SW_APPLET_SELECT_FAILED =
+            new StatusWord(0x6999, "Applet selection failed");
+
+    public static final StatusWord SW_WRONG_DATA =
+            new StatusWord(0x6A80, "Wrong data");
+
+    public static final StatusWord SW_FUNCTION_NOT_SUPPORTED =
+            new StatusWord(0x6A81, "Function not supported");
+
+    public static final StatusWord SW_FILE_NOT_FOUND =
+            new StatusWord(0x6A82, "File not found");
+
+    public static final StatusWord SW_RECORD_NOT_FOUND =
+            new StatusWord(0x6A83, "Record not found");
+
+    public static final StatusWord SW_NOT_ENOUGH_MEMORY =
+            new StatusWord(0x6A84, "Not enough memory");
+
+    public static final StatusWord SW_NC_INCONSISTENT_WITH_TLV =
+            new StatusWord(0x6A85, "Nc inconsistent with TLV structure");
+
+    public static final StatusWord SW_INCORRECT_P1P2 =
+            new StatusWord(0x6A86, "Incorrect P1 or P2");
+
+    public static final StatusWord SW_DATA_NOT_FOUND =
+            new StatusWord(0x6A88, "Referenced data not found");
+
+    public static final StatusWord SW_FILE_ALREADY_EXISTS =
+            new StatusWord(0x6A89, "File already exists");
+
+    public static final StatusWord SW_WRONG_P1P2 =
+            new StatusWord(0x6B00, "Wrong P1 or P2");
+
+    public static final StatusWord SW_WRONG_LE =
+            new StatusWord(0x6C00, "Wrong Le");
+
+    public static final StatusWord SW_INS_NOT_SUPPORTED =
+            new StatusWord(0x6D00, "Instruction not supported or invalid");
+
+    public static final StatusWord SW_CLA_NOT_SUPPORTED =
+            new StatusWord(0x6E00, "Class not supported");
+
+    public static final StatusWord SW_UNKNOWN_ERROR =
+            new StatusWord(0x6F00, "Unknown error (no precise diagnosis)");
+
+    private static final String UNKNOWN_STATUS_WORD_MESSAGE = "Unknown status word";
+
+    public static final ImmutableSet<StatusWord> ALL_KNOWN_STATUS_WORDS =
+            ImmutableSet.of(
+                    SW_NO_ERROR,
+                    SW_RESPONSE_BYTES_STILL_AVAILABLE,
+                    SW_WARNING_STATE_UNCHANGED,
+                    SW_CARD_MANAGER_LOCKED,
+                    SW_WARNING_NO_INFO_GIVEN,
+                    SW_WARNING_MORE_DATA,
+                    SW_VERIFY_FAILED,
+                    SW_NO_SPECIFIC_DIAGNOSTIC,
+                    SW_REQUESTED_ELEMENTS_NOT_AVAILABLE,
+                    SW_ICA_ALREADY_EXISTS,
+                    SW_WRONG_LENGTH,
+                    SW_SECURITY_STATUS_NOT_SATISFIED,
+                    SW_FILE_INVALID,
+                    SW_REFERENCE_DATA_NOT_USABLE,
+                    SW_CONDITIONS_NOT_SATISFIED,
+                    SW_COMMAND_NOT_ALLOWED,
+                    SW_APPLET_SELECT_FAILED,
+                    SW_WRONG_DATA,
+                    SW_FUNCTION_NOT_SUPPORTED,
+                    SW_FILE_NOT_FOUND,
+                    SW_RECORD_NOT_FOUND,
+                    SW_NOT_ENOUGH_MEMORY,
+                    SW_NC_INCONSISTENT_WITH_TLV,
+                    SW_INCORRECT_P1P2,
+                    SW_DATA_NOT_FOUND,
+                    SW_FILE_ALREADY_EXISTS,
+                    SW_WRONG_P1P2,
+                    SW_WRONG_LE,
+                    SW_INS_NOT_SUPPORTED,
+                    SW_CLA_NOT_SUPPORTED,
+                    SW_UNKNOWN_ERROR);
+
+    /** A meessage that is used to construct an exception to represent this status word. */
+    private final String mMessage;
+
+    /** The actual status word (2 bytes). */
+    private final int mStatusWord;
+
+    /** Map status words to values for fast lookup. */
+    private static final Map<Integer, StatusWord> STATUS_WORD_MAP;
+
+    static {
+        // Map all the values to their code.
+        Map<Integer, StatusWord> statusWordMap = new LinkedHashMap<>(ALL_KNOWN_STATUS_WORDS.size());
+        for (StatusWord value : ALL_KNOWN_STATUS_WORDS) {
+            statusWordMap.put(Integer.valueOf(value.mStatusWord), value);
+        }
+        STATUS_WORD_MAP = Collections.unmodifiableMap(statusWordMap);
+    }
+
+    private StatusWord(int sw, String message) {
+        mStatusWord = sw;
+        this.mMessage = message;
+    }
+
+    /** Lookup a {@link StatusWord} from the status word value. */
+    public static StatusWord fromInt(int sw) {
+        Preconditions.checkArgument((sw >> Short.SIZE) == 0);
+        StatusWord statusWord = STATUS_WORD_MAP.get(Integer.valueOf(sw));
+        if (statusWord != null) {
+            return statusWord;
+        }
+        return new StatusWord(sw, UNKNOWN_STATUS_WORD_MESSAGE);
+    }
+
+    /**
+     * Gets the byte array form of the status word.
+     */
+    public byte[] toBytes() {
+        Preconditions.checkState((mStatusWord >> Short.SIZE) == 0);
+        return Shorts.toByteArray((short) mStatusWord);
+    }
+
+    /**
+     * Gets the int value of the status word.
+     */
+    public int toInt() {
+        return mStatusWord;
+    }
+
+    /**
+     * Gets the description message of the status word.
+     */
+    public String getMessage() {
+        return mMessage;
+    }
+
+    /**
+     * Checks if this status word is known.
+     */
+    public boolean isKnown() {
+        return ALL_KNOWN_STATUS_WORDS.contains(this);
+    }
+
+    /**
+     * Checks if the given status words are known.
+     */
+    public static boolean areAllKnown(Iterable<StatusWord> statusWords) {
+        for (StatusWord word : statusWords) {
+            if (!word.isKnown()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("'%04X': %s", mStatusWord, mMessage);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj == null || obj.getClass() != this.getClass()) {
+            return false;
+        }
+
+        StatusWord other = (StatusWord) obj;
+        return other.mStatusWord == this.mStatusWord;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mStatusWord);
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/iso7816/TlvDatum.java b/service/java/com/android/server/uwb/secure/iso7816/TlvDatum.java
new file mode 100644
index 0000000..cad7b1e
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/iso7816/TlvDatum.java
@@ -0,0 +1,218 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.iso7816;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.server.uwb.util.DataTypeConversionUtil;
+import com.android.server.uwb.util.Hex;
+
+import com.google.common.primitives.Bytes;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+// TODO(b/214552182): refactor to be compatible with TlvBuffer.
+/**
+ * Wrapper on a singular datum stored in a TLV byte string. Defined in section 6.2 of
+ * ISO/IEC 7816-4: 2020 standard.
+ */
+public class TlvDatum {
+    private static final String LOG_TAG = "TlvDatum";
+
+    public static final int MAX_SIZE_SINGLE_BYTE = 0x7F;
+    public static final int MAX_SIZE_TWO_BYTE = 0xFF;
+    public static final int MAX_SIZE_THREE_BYTE = 0xFFFF;
+    public static final int MAX_SIZE_FOUR_BYTE = 0xFFFFFF;
+    public static final int MAX_SIZE_FIVE_BYTE = 0xFFFFFFFF;
+
+    public static final byte TWO_BYTES_LEN_FIRST_BYTE = (byte) 0x81;
+    public static final byte THREE_BYTES_LEN_FIRST_BYTE = (byte) 0x82;
+    public static final byte FOUR_BYTES_LEN_FIRST_BYTE = (byte) 0x83;
+    public static final byte FIVE_BYTES_LEN_FIRST_BYTE = (byte) 0x84;
+
+    // The tag of the TLV structure.
+    @NonNull public final Tag tag;
+    // For constructed data objects, this will be the byte representation of subTlvData
+    @NonNull public final byte[] value;
+    // Will only be non-empty for constructed (i.e., non-primitive) data objects.
+    @NonNull public final Map<Tag, List<TlvDatum>> subTlvData;
+
+    /**
+     * Constructor of TlvDatum.
+     */
+    public TlvDatum(@NonNull Tag tag, @NonNull Map<Tag, List<TlvDatum>> subTlvData) {
+        this.tag = tag;
+        this.subTlvData = subTlvData;
+
+        byte[] value = new byte[] {};
+        for (Map.Entry<Tag, List<TlvDatum>> subTlvs : subTlvData.entrySet()) {
+            for (TlvDatum subTlvDatum : subTlvs.getValue()) {
+                value = Bytes.concat(value, subTlvDatum.toBytes());
+            }
+        }
+
+        this.value = value;
+    }
+
+    /**
+     * Constructor of TlvDatum with a sub TlvDatum.
+     */
+    public TlvDatum(@NonNull Tag tag, @NonNull TlvDatum subTlvDatum) {
+        this.tag = tag;
+        this.value = subTlvDatum.toBytes();
+        this.subTlvData = new HashMap<Tag, List<TlvDatum>>();
+        subTlvData.put(subTlvDatum.tag, Arrays.asList(subTlvDatum));
+    }
+
+    /**
+     * Constructor of TlvDatum.
+     */
+    public TlvDatum(@NonNull Tag tag, @NonNull byte[] value) {
+        this.tag = tag;
+        this.value = value;
+        this.subTlvData = new HashMap<Tag, List<TlvDatum>>();
+    }
+
+    /**
+     * Constructor of TlvDatum.
+     */
+    public TlvDatum(@NonNull Tag tag, int value) {
+        this.tag = tag;
+        this.value = DataTypeConversionUtil.i32ToByteArray(value);
+        this.subTlvData = new HashMap<Tag, List<TlvDatum>>();
+    }
+
+    /**
+     * Convert the TLV to byte array.
+     */
+    public byte[] toBytes() {
+        // determine number of bytes to use for length
+        int sizeByteLength = 1;
+        if (value.length > MAX_SIZE_FIVE_BYTE) {
+            Log.wtf(LOG_TAG, "The length of data is over limit for tag: " + tag);
+        }
+        if (value.length > MAX_SIZE_FOUR_BYTE) {
+            sizeByteLength = 5;
+        } else if (value.length > MAX_SIZE_THREE_BYTE) {
+            sizeByteLength = 4;
+        } else if (value.length > MAX_SIZE_TWO_BYTE) {
+            sizeByteLength = 3;
+        } else if (value.length > MAX_SIZE_SINGLE_BYTE) {
+            sizeByteLength = 2;
+        }
+
+        return Bytes.concat(tag.literalValue, lengthToBytes(sizeByteLength, value.length), value);
+    }
+
+    private static byte[] lengthToBytes(int sizeByteLength, int size) {
+        switch (sizeByteLength) {
+            case 1:
+                return new byte[] {DataTypeConversionUtil.unsignedIntToByte(size)};
+
+            case 2:
+                return new byte[] {TWO_BYTES_LEN_FIRST_BYTE,
+                        DataTypeConversionUtil.unsignedIntToByte(size)};
+
+            case 3:
+                return new byte[] {
+                        THREE_BYTES_LEN_FIRST_BYTE,
+                        DataTypeConversionUtil.unsignedIntToByte(size >> 8),
+                        DataTypeConversionUtil.unsignedIntToByte(size)
+                };
+
+            case 4:
+                return new byte[] {
+                        FOUR_BYTES_LEN_FIRST_BYTE,
+                        DataTypeConversionUtil.unsignedIntToByte(size >> 16),
+                        DataTypeConversionUtil.unsignedIntToByte(size >> 8),
+                        DataTypeConversionUtil.unsignedIntToByte(size)
+                };
+
+            case 5:
+                return new byte[] {
+                        FIVE_BYTES_LEN_FIRST_BYTE,
+                        DataTypeConversionUtil.unsignedIntToByte(size >> 24),
+                        DataTypeConversionUtil.unsignedIntToByte(size >> 16),
+                        DataTypeConversionUtil.unsignedIntToByte(size >> 8),
+                        DataTypeConversionUtil.unsignedIntToByte(size)
+                };
+
+            default:
+                throw new IndexOutOfBoundsException(
+                        "length of " + sizeByteLength + " not supported");
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringWriter stringWriter = new StringWriter();
+        PrintWriter printWriter = new PrintWriter(stringWriter);
+
+        printWriter.printf("TlvDatum : TAG=[%s], VALUE=[%s]",
+                Hex.encode(tag.literalValue), Hex.encode(value));
+
+        return stringWriter.toString();
+    }
+
+    /**
+     * The Tag of TLV(Tag, Length, Value) data structure.
+     */
+    public static class Tag {
+        @NonNull
+        public final byte[] literalValue;
+        public Tag(@NonNull byte[] literalValue) {
+            this.literalValue = literalValue;
+        }
+
+        public Tag(byte value) {
+            this.literalValue = new byte[] { value };
+        }
+
+        public Tag(byte firstByte, byte secondByte) {
+            this.literalValue = new byte[] {firstByte, secondByte};
+        }
+
+        @Override
+        public String toString() {
+            return DataTypeConversionUtil.byteArrayToHexString(literalValue);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object that) {
+            if (this == that) {
+                return true;
+            }
+            if (that == null || that.getClass() != this.getClass()) {
+                return false;
+            }
+
+            return Arrays.equals(literalValue, ((Tag) that).literalValue);
+        }
+
+        @Override
+        public int hashCode() {
+            return Arrays.hashCode(literalValue);
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/secure/iso7816/TlvParser.java b/service/java/com/android/server/uwb/secure/iso7816/TlvParser.java
new file mode 100644
index 0000000..4abe7e4
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/iso7816/TlvParser.java
@@ -0,0 +1,168 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.iso7816;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import com.google.common.primitives.Bytes;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class for parsing TLV (Tag, Length, Value) data.
+ *
+ * <p>TLV objects are structured as [tag][length][value]. The [tag] is either 1 or 2 bytes and
+ * specifies what the value means (e.g., credit card number) and how it is encoded (e.g., ASCII).
+ * The [length] is 1-3 bytes and specifies how long the [value] field is. The [value] field is the
+ * value of the object and is decoded depending on the [tag].
+ */
+public class TlvParser {
+    private static class ByteArrayWrapper {
+        private final ByteBuffer mByteBuffer;
+
+        ByteArrayWrapper(byte[] byteArray) {
+            this.mByteBuffer = ByteBuffer.wrap(byteArray);
+        }
+
+        /**
+         * Read the part of the data in the array from the current offset.
+         */
+        private byte[] read(int bytes) throws IOException {
+            byte[] result = new byte[bytes];
+            try {
+                mByteBuffer.get(result);
+            } catch (BufferUnderflowException e) {
+                throw new IOException("Not enough bytes");
+            }
+            return result;
+        }
+    }
+
+    /**
+     * Parses bytes from a stream interface to a TlvDatum wrapper object.
+     *
+     * @param byteArrayWrapper byte stream provider
+     * @return TlvDatum derived from the data.
+     */
+    @Nullable
+    private static TlvDatum parseOneTlv(ByteArrayWrapper byteArrayWrapper) {
+        try {
+            return parseTlv(byteArrayWrapper);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Parses bytes from a stream interface to TlvDatum wrapper objects until consumed.
+     *
+     * @param stream byte stream provider
+     * @return The map of tag and TlvDatum derived from the data.
+     */
+    private static Map<Tag, List<TlvDatum>> parseTlvs(ByteArrayWrapper byteArrayWrapper) {
+        Map<Tag, List<TlvDatum>> tlvData = new HashMap<>();
+        TlvDatum tlvDatum;
+
+        while ((tlvDatum = parseOneTlv(byteArrayWrapper)) != null) {
+            List<TlvDatum> tlvs = tlvData.computeIfAbsent(
+                    tlvDatum.tag, (k) -> new ArrayList<>());
+            tlvs.add(tlvDatum);
+        }
+
+        return tlvData;
+    }
+
+    /**
+     * Parses the message bytes of a command APDU into a TlvDatum wrapper object.
+     *
+     * @param command the command APDU.
+     * @return TlvDatum list of TlvDatum derived from the data.
+     */
+    public static Map<Tag, List<TlvDatum>> parseTlvs(CommandApdu command) {
+        return parseTlvs(command.getCommandData());
+    }
+
+    /**
+     * Parses the message bytes of a response APDU into a TlvDatum wrapper object.
+     *
+     * @param response the response APDU.
+     * @return TlvDatum list of TlvDatum derived from the data.
+     */
+    public static Map<Tag, List<TlvDatum>> parseTlvs(ResponseApdu response) {
+        return parseTlvs(response.getResponseData());
+    }
+
+    /**
+     * Parses a byte array message into a TlvDatum wrapper object.
+     *
+     * @param message message byte array to be parsed.
+     * @return TlvDatum list of TlvDatum derived from the data.
+     */
+    public static Map<Tag, List<TlvDatum>> parseTlvs(byte[] message) {
+        return parseTlvs(new ByteArrayWrapper(message));
+    }
+
+    private static TlvDatum parseTlv(ByteArrayWrapper byteArrayWrapper) throws IOException {
+        byte[] tag = byteArrayWrapper.read(/* bytes= */ 1);
+        // When first byte is of the form 0bXXX11111, the tag contains a 2nd byte.
+        if (((tag[0] + 1) & 0b00011111) == 0) {
+            tag = Bytes.concat(tag, byteArrayWrapper.read(/* bytes= */ 1));
+        }
+
+        byte[] lengthBytes = byteArrayWrapper.read(/* bytes= */ 1);
+        switch (lengthBytes[0]) {
+            case TlvDatum.TWO_BYTES_LEN_FIRST_BYTE:
+                lengthBytes = byteArrayWrapper.read(/* bytes= */ 1);
+                break;
+            case TlvDatum.THREE_BYTES_LEN_FIRST_BYTE:
+                lengthBytes = byteArrayWrapper.read(/* bytes= */ 2);
+                break;
+            case TlvDatum.FOUR_BYTES_LEN_FIRST_BYTE:
+                lengthBytes = byteArrayWrapper.read(/* bytes= */ 3);
+                break;
+            case TlvDatum.FIVE_BYTES_LEN_FIRST_BYTE:
+                lengthBytes = byteArrayWrapper.read(/* bytes= */ 4);
+                break;
+            default: // fall out
+        }
+        int length = DataTypeConversionUtil.arbitraryByteArrayToI32(lengthBytes);
+
+        byte[] value = byteArrayWrapper.read(length);
+        if (isConstructedTag(tag[0])) {
+            return new TlvDatum(new Tag(tag), parseTlvs(value));
+        } else {
+            return new TlvDatum(new Tag(tag), value);
+        }
+    }
+
+    private static boolean isConstructedTag(byte firstTagByte) {
+        // If 6th bit is 1, then data object is constructed; otherwise it is primitive.
+        // A constructed object's value field contains more TLV structures, while a primitive
+        // object's data field does not (contains only data).
+        return (firstTagByte & 0b00100000) != 0;
+    }
+
+    private TlvParser() {}
+}
diff --git a/service/java/com/android/server/uwb/secure/omapi/OmapiConnection.java b/service/java/com/android/server/uwb/secure/omapi/OmapiConnection.java
new file mode 100644
index 0000000..8c6b6e8
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/omapi/OmapiConnection.java
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.omapi;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.server.uwb.secure.iso7816.CommandApdu;
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+
+import java.io.IOException;
+
+/** Interface for using OMAPI to communicate with a secure element applet with APDUs */
+@WorkerThread
+public interface OmapiConnection {
+    /** Callback for listening to initialization completion event. */
+    @WorkerThread
+    public interface InitCompletionCallback {
+        /** Called when initializtion is completed. */
+        void onInitCompletion();
+    }
+
+    /** Initialize the connection. */
+    void init(InitCompletionCallback callback);
+
+    /** Transmits the given CommandApdu to the secure element */
+    ResponseApdu transmit(CommandApdu command) throws IOException;
+
+    /** Opens a logical channel to the FiRa applet. */
+    ResponseApdu openChannel() throws IOException;
+
+    /** Closes all channels to the SE. */
+    void closeChannel() throws IOException;
+}
diff --git a/service/java/com/android/server/uwb/secure/omapi/OmapiConnectionImpl.java b/service/java/com/android/server/uwb/secure/omapi/OmapiConnectionImpl.java
new file mode 100644
index 0000000..25c5b7c
--- /dev/null
+++ b/service/java/com/android/server/uwb/secure/omapi/OmapiConnectionImpl.java
@@ -0,0 +1,276 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.omapi;
+
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_NO_ERROR;
+import static com.android.server.uwb.secure.iso7816.StatusWord.SW_NO_SPECIFIC_DIAGNOSTIC;
+import static com.android.server.uwb.util.Constants.FIRA_APPLET_AID;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import android.content.Context;
+import android.se.omapi.Channel;
+import android.se.omapi.Reader;
+import android.se.omapi.SEService;
+import android.se.omapi.Session;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
+
+import com.android.server.uwb.secure.iso7816.CommandApdu;
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import java.io.IOException;
+import java.util.NoSuchElementException;
+import java.util.concurrent.Executor;
+
+/** Object for creating an actual connection to a secure element through the OMAPI layer. */
+@WorkerThread
+public class OmapiConnectionImpl implements OmapiConnection {
+    private static final String LOG_TAG = "OmapiConnectionImpl";
+
+    private final Context mContext;
+    private final Executor mSyncExecutor = (runnable) -> runnable.run();
+
+    @VisibleForTesting
+    @Nullable SEService mSeService;
+    @Nullable private Session mSession;
+    @Nullable private Reader mReader;
+    @Nullable private Channel mChannel;
+
+    OmapiConnectionImpl(
+            Context context) {
+        this.mContext = context;
+    }
+
+    /**
+     * Initializes the connection to SeService.
+     */
+    @Override
+    public void init(OmapiConnection.InitCompletionCallback callback) {
+        if (this.mSeService == null) {
+            this.mSeService =
+                    new SEService(
+                            mContext,
+                            mSyncExecutor,
+                            () -> {
+                                callback.onInitCompletion();
+                            });
+        }
+    }
+
+    /**
+     * Transmits the command APDU to FiRa applet.
+     */
+    @NonNull
+    @Override
+    public ResponseApdu transmit(CommandApdu command) throws IOException {
+        byte[] response = transmit(command.getEncoded());
+        ResponseApdu responseApdu = ResponseApdu.fromResponse(response);
+        return responseApdu;
+    }
+
+    /**
+     * Transmits the given bytes to the SE
+     *
+     * @param command bytes to transmit, must consist a valid command as a response will be received
+     *     from the SE and returned to the caller
+     * @return the APDU response received from the SE
+     */
+    private byte[] transmit(byte[] command) throws IOException {
+        if (mChannel == null) {
+            throw new IOException("No active channel found.");
+        }
+
+        return mChannel.transmit(command);
+    }
+
+    /**
+     * Closes the logical channel to the FiRa applet.
+     * @throws IOException
+     */
+    @Override
+    public void closeChannel() throws IOException {
+        Session session = getSession();
+        if (session == null) {
+            logw("Cannot close channel without a Session.");
+        } else {
+            session.closeChannels();
+            mChannel = null;
+        }
+    }
+
+    /**
+     * Gets tthe debug information for the current OMAPI connection.
+     */
+    public String getDebugInfo() {
+        StringBuilder res = new StringBuilder();
+        SEService seService = getSecureElementService();
+        if (seService == null) {
+            res.append("Could not get SEService");
+        } else {
+            res.append("Readers: \n");
+            for (Reader reader : seService.getReaders()) {
+                logi("Found reader: " + reader.getName());
+                res.append("\tName: ")
+                        .append(reader.getName())
+                        .append(" isSecureElementPresent: ")
+                        .append(reader.isSecureElementPresent());
+            }
+        }
+
+        return res.toString();
+    }
+
+    @Nullable
+    private SEService getSecureElementService() {
+        if (mContext == null) {
+            logd("The SEService is not initialized.");
+            return null;
+        }
+
+        if (mSeService == null || !mSeService.isConnected()) {
+            logd("OMAPI SEService is not connected.");
+            return null;
+        }
+
+        return mSeService;
+    }
+
+    /**
+     * Opens the logical channel to the FiRa applet, called once for each secure session.
+     * @throws IOException
+     */
+    @NonNull
+    @Override
+    public ResponseApdu openChannel() throws IOException {
+        if (mChannel != null) {
+            // Repeated SELECT operations are not supported and indicative of leaky code.
+            logw("Repeated SELECT operations are not supported.");
+            return ResponseApdu.fromStatusWord(SW_NO_SPECIFIC_DIAGNOSTIC);
+        }
+
+        byte[] response = null;
+        Session session = getSession();
+        if (session == null) {
+            logw("Cannot open a Channel without a Session.");
+        } else {
+            try {
+                mChannel = session.openLogicalChannel(FIRA_APPLET_AID);
+            } catch (SecurityException | NoSuchElementException | UnsupportedOperationException e) {
+                logw("Exception trying to talk to DCK Applet");
+                throw new IOException(e);
+            }
+            logi("Logical channel opened for AID: "
+                    + DataTypeConversionUtil.byteArrayToHexString(FIRA_APPLET_AID));
+            checkNotNull(mChannel);
+            response = mChannel.getSelectResponse();
+            logi("Channel open response: "
+                    + DataTypeConversionUtil.byteArrayToHexString(response));
+        }
+
+        if (response == null || response.length == 0) {
+            throw new IOException("Null response received from channel open.");
+        }
+
+        ResponseApdu responseApdu = ResponseApdu.fromResponse(response);
+        return responseApdu;
+    }
+
+    @Nullable
+    private Session getSession() throws IOException {
+        if (mSession == null) {
+            Reader reader = getReader();
+            if (reader == null) {
+                logw("Cannot get Session without Reader.");
+            } else {
+                logi("Opening session with reader: " + reader.getName());
+                mSession = reader.openSession();
+            }
+        }
+        return mSession;
+    }
+
+    @Nullable
+    private Reader getReader() throws IOException {
+        if (mReader == null) {
+            SEService seService = getSecureElementService();
+            if (seService == null) {
+                logw("SEService not connected. Cannot get Reader without SEService.");
+            } else {
+                for (Reader r : seService.getReaders()) {
+                    if (r.getName().startsWith("eSE")) {
+                        if (checkFiRaAppletPresence(r)) {
+                            mReader = r;
+                            break;
+                        }
+                    }
+                }
+                if (mReader == null) {
+                    logw("Unable to find or select applet.");
+                    throw new IOException("FiRa applet not found");
+                }
+            }
+        }
+        return mReader;
+    }
+
+    private boolean checkFiRaAppletPresence(Reader reader) {
+        this.mReader = reader;
+        try {
+            ResponseApdu selectResponse = openChannel();
+            closeChannel();
+            if (selectResponse.getStatusWord() == SW_NO_ERROR.toInt()) {
+                logi("FiRa applet found with reader: " +  reader.getName());
+                return true;
+            } else {
+                logw("Unable to select applet or applet not found with reader: "
+                        + reader.getName()
+                        + "Received response to"
+                        + " SELECT: "
+                        + selectResponse);
+            }
+        } catch (IOException e) {
+            logw("IOException happened with reader: " + reader.getName());
+        }
+
+        logw("Error selecting FiRa applet (or applet not present) on reader: "
+                + reader.getName());
+
+        this.mReader = null;
+        if (mSession != null) {
+            mSession.close();
+        }
+        this.mSession = null;
+        return false;
+    }
+
+    private void logd(String log) {
+        Log.d(LOG_TAG, log);
+    }
+
+    private void logw(String log) {
+        Log.w(LOG_TAG, log);
+    }
+
+    private void logi(String log) {
+        Log.i(LOG_TAG, log);
+    }
+}
diff --git a/service/java/com/android/server/uwb/test/UwbTestLoopBackTestResult.java b/service/java/com/android/server/uwb/test/UwbTestLoopBackTestResult.java
new file mode 100644
index 0000000..42db487
--- /dev/null
+++ b/service/java/com/android/server/uwb/test/UwbTestLoopBackTestResult.java
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+
+/* UwbTestLoopBackTestResult is unused now*/
+/*package com.android.server.uwb.test;
+
+import com.android.server.uwb.util.UwbUtil;
+
+public class UwbTestLoopBackTestResult {
+    public int mStatus;
+    public  long mTxtsInt;
+    public  int mTxtsFrac;
+    public  long mRxtsInt;
+    public  int mRxtsFrac;
+    public  float mAoaAzimuth;
+    public  float mAoaElevation;
+    public  int mPhr;
+    public  byte[] mPsduData;
+
+    *//* Vendor Specific Data *//*
+    public byte[] mVendorExtnData;
+
+    public UwbTestLoopBackTestResult(int status, long txtsInt, int txtsFrac, long rxtsInt,
+            int rxtsFrac, int aoaAzimuth, int aoaElevation,  int phr, byte[] psduData,
+            byte[] vendorExtnData) {
+        *//* Vendor Specific data  *//*
+        this.mStatus = status;
+        this.mTxtsInt = txtsInt;
+        this.mTxtsFrac = txtsFrac;
+        this.mRxtsInt = rxtsInt;
+        this.mRxtsFrac = rxtsFrac;
+        this.mAoaAzimuth =
+                UwbUtil.convertQFormatToFloat(UwbUtil.twos_compliment(aoaAzimuth, 16), 9, 7);
+        this.mAoaElevation =
+                UwbUtil.convertQFormatToFloat(UwbUtil.twos_compliment(aoaElevation, 16), 9, 7);
+        this.mPhr = phr;
+        this.mPsduData = psduData;
+
+        *//* Vendor Specific Data *//*
+        this.mVendorExtnData = vendorExtnData;
+
+    }
+
+    public int getStatus() {
+        return mStatus;
+    }
+
+
+    public long getTxTsInt() {
+        return mTxtsInt;
+    }
+
+    public int getTxTsFrac() {
+        return mTxtsFrac;
+    }
+
+    public long getRxTsInt() {
+        return mRxtsInt;
+    }
+
+    public int getRxTsFrac() {
+        return mRxtsFrac;
+    }
+
+    public float getAoaAzimuth() {
+        return mAoaAzimuth;
+    }
+
+    public float getAoaElevation() {
+        return mAoaElevation;
+    }
+
+    public int getPhr() {
+        return mPhr;
+    }
+
+    public byte[] getPsduData() {
+        return mPsduData;
+    }
+
+    *//* Vendor Specific Data *//*
+
+    public byte[] getVendorExtnData() {
+        return mVendorExtnData;
+    }
+
+    @Override
+    public String toString() {
+        return " UwbTestLoopBackTestResult { "
+                + " Status = " + mStatus
+                + ", TxtsInt = " + mTxtsInt
+                + ", TxtsFrac = " + mTxtsFrac
+                + ", RxtsInt = " + mRxtsInt
+                + ", RxtsFrac = " + mRxtsFrac
+                + ", AoaAzimuth = " + mAoaAzimuth
+                + ", AoaElevation = " + mAoaElevation
+                + ", Phr = " + mPhr
+                + ", PsduData = " + UwbUtil.toHexString(mPsduData)
+                + *//* Vendor Specific Data *//*
+                ", VendorExtnData = " + UwbUtil.toHexString(mVendorExtnData)
+                + '}';
+    }
+}*/
diff --git a/service/java/com/android/server/uwb/test/UwbTestPeriodicTxResult.java b/service/java/com/android/server/uwb/test/UwbTestPeriodicTxResult.java
new file mode 100644
index 0000000..bea32c8
--- /dev/null
+++ b/service/java/com/android/server/uwb/test/UwbTestPeriodicTxResult.java
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+/* UwbTestPeriodicTxResult is unused now*/
+/*package com.android.server.uwb.test;
+
+public class UwbTestPeriodicTxResult {
+    public int mStatus;
+
+    public UwbTestPeriodicTxResult(int status) {
+        this.mStatus = status;
+    }
+
+    public int getStatus() {
+        return mStatus;
+    }
+
+    public void setStatus(int status) {
+        this.mStatus = status;
+    }
+
+    @Override
+    public String toString() {
+        return "UwbTestPeriodicTxResult { "
+                + " Status = " + mStatus
+                + '}';
+    }
+}*/
diff --git a/service/java/com/android/server/uwb/test/UwbTestRxPacketErrorRateResult.java b/service/java/com/android/server/uwb/test/UwbTestRxPacketErrorRateResult.java
new file mode 100644
index 0000000..636a7cb
--- /dev/null
+++ b/service/java/com/android/server/uwb/test/UwbTestRxPacketErrorRateResult.java
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+
+/* UwbTestRxPacketErrorRateResult is unused now*/
+/*package com.android.server.uwb.test;
+
+import com.android.server.uwb.util.UwbUtil;
+
+public class UwbTestRxPacketErrorRateResult {
+    public int mStatus;
+    public long mAttempts;
+    public long mAcqDetect;
+    public long mAcqReject;
+    public long mRxFail;
+    public long mSyncCirReady;
+    public long mSfdFail;
+    public long mSfdFound;
+    public long mPhrDecError;
+    public long mPhrBitError;
+    public long mPsduDecError;
+    public long mPsduBitError;
+    public long mStsFound;
+    public long mEof;
+    *//* Vendor Specific Data *//*
+    public byte[] mVendorExtnData;
+
+    public UwbTestRxPacketErrorRateResult(int status, long attempts, long acqDetect,
+            long acqReject, long rxFail, long syncCirReady, long sfdFail, long sfdFound,
+            long phrDecError, long phrBitError, long psduDecError, long psduBitError, long stsFound,
+            long eof, byte[] vendorExtnData) {
+        this.mStatus = status;
+        this.mAttempts = attempts;
+        this.mAcqDetect = acqDetect;
+        this.mAcqReject = acqReject;
+        this.mRxFail = rxFail;
+        this.mSyncCirReady = syncCirReady;
+        this.mSfdFail = sfdFail;
+        this.mSfdFound = sfdFound;
+        this.mPhrDecError = phrDecError;
+        this.mPhrBitError = phrBitError;
+        this.mPsduDecError = psduDecError;
+        this.mPsduBitError = psduBitError;
+        this.mStsFound = stsFound;
+        this.mEof = eof;
+
+        *//* Vendor Specific Data *//*
+        this.mVendorExtnData = vendorExtnData;
+    }
+
+    public int getStatus() {
+        return mStatus;
+    }
+
+    public long getAttempts() {
+        return mAttempts;
+    }
+
+    public long getAcqDetect() {
+        return mAcqDetect;
+    }
+
+    public long getAcqReject() {
+        return mAcqReject;
+    }
+
+    public long getRxFail() {
+        return mRxFail;
+    }
+
+    public long getSyncCirReady() {
+        return mSyncCirReady;
+    }
+
+    public long getSfdFail() {
+        return mSfdFail;
+    }
+
+    public long getSfdFound() {
+        return mSfdFound;
+    }
+
+    public long getPhrDecError() {
+        return mPhrDecError;
+    }
+
+    public long getPhrBitError() {
+        return mPhrBitError;
+    }
+
+    public long getPsduDecError() {
+        return mPsduDecError;
+    }
+
+    public long getPsduBitError() {
+        return mPsduBitError;
+    }
+
+    public long getStsFound() {
+        return mStsFound;
+    }
+
+    public long getEof() {
+        return mEof;
+    }
+
+    *//* Vendor Specific Data *//*
+
+    public byte[] getVendorExtnData() {
+        return mVendorExtnData;
+    }
+
+
+    @Override
+    public String toString() {
+        return " UwbTestRxPacketErrorRateResult { "
+                + " Status = " + mStatus
+                + ", Attempts = " + mAttempts
+                + ", AcqDetect = " + mAcqDetect
+                + ", AcqReject = " + mAcqReject
+                + ", RxFail = " + mRxFail
+                + ", SyncCirReady = " + mSyncCirReady
+                + ", SfdFail = " + mSfdFail
+                + ", SfdFound = " + mSfdFound
+                + ", PhrDecError = " + mPhrDecError
+                + ", PhrBitError = " + mPhrBitError
+                + ", PsduDecError = " + mPsduDecError
+                + ", PsduBitError = " + mPsduBitError
+                + ", StsFound = " + mStsFound
+                + ", Eof = " + mEof
+                + ", VendorExtnData = " + UwbUtil.toHexString(mVendorExtnData)
+                + '}';
+    }
+
+}*/
diff --git a/service/java/com/android/server/uwb/test/UwbTestRxResult.java b/service/java/com/android/server/uwb/test/UwbTestRxResult.java
new file mode 100644
index 0000000..ab97eae
--- /dev/null
+++ b/service/java/com/android/server/uwb/test/UwbTestRxResult.java
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+/* UwbTestRxResult is unused now*/
+/*package com.android.server.uwb.test;
+
+import com.android.server.uwb.util.UwbUtil;
+
+public class UwbTestRxResult {
+    public int mStatus;
+    public long mRxDoneTsInt;
+    public int mRxDoneTsFrac;
+    public float mAoaAzimuth;
+    public float mAoaElevation;
+    public int mToaGap;
+    public int mPhr;
+    public byte[] mPsduData;
+    public byte[] mVendorExtnData;
+
+    public UwbTestRxResult(int status, long rxDoneTsInt, int rxDoneTsFrac,
+            int aoaAzimuth, int aoaElevation, int toaGap, int phr, byte[] psduData,
+            byte[] vendorExtnData) {
+
+        this.mStatus = status;
+        this.mRxDoneTsInt = rxDoneTsInt;
+        this.mRxDoneTsFrac = rxDoneTsFrac;
+        this.mAoaAzimuth =
+                UwbUtil.convertQFormatToFloat(UwbUtil.twos_compliment(aoaAzimuth, 16), 9, 7);
+        this.mAoaElevation =
+                UwbUtil.convertQFormatToFloat(UwbUtil.twos_compliment(aoaElevation, 16), 9, 7);
+        this.mToaGap = toaGap;
+        this.mPhr = phr;
+        this.mPsduData = psduData;
+
+        *//* Vendor Specific Data *//*
+        this.mVendorExtnData = vendorExtnData;
+
+    }
+
+    public int getStatus() {
+        return mStatus;
+    }
+
+    public long getRxDoneTsInt() {
+        return mRxDoneTsInt;
+    }
+
+    public int getRxDoneTsFrac() {
+        return mRxDoneTsFrac;
+    }
+
+    public float getAoaAzimuth() {
+        return mAoaAzimuth;
+    }
+
+    public float getAoaElevation() {
+        return mAoaElevation;
+    }
+
+    public int getToaGap() {
+        return mToaGap;
+    }
+
+    public int getPhr() {
+        return mPhr;
+    }
+
+    public byte[] getPsduData() {
+        return mPsduData;
+    }
+
+    *//* Vendor Specific Data *//*
+
+    public byte[] getVendorExtnData() {
+        return mVendorExtnData;
+    }
+
+    @Override
+    public String toString() {
+        return " UwbTestRxResult { "
+                + " Status = " + mStatus
+                + ", RxDoneTsInt = " + mRxDoneTsInt
+                + ", RxDoneTsFrac = " + mRxDoneTsFrac
+                + ", AoaAzimuth = " + mAoaAzimuth
+                + ", AoaElevation = " + mAoaElevation
+                + ", ToaGap = " + mToaGap
+                + ", Phr = " + mPhr
+                + ", PsduData = " + UwbUtil.toHexString(mPsduData)
+                + ", VendorExtnData = " + UwbUtil.toHexString(mVendorExtnData)
+                + '}';
+    }
+}*/
diff --git a/service/java/com/android/server/uwb/util/ArrayUtils.java b/service/java/com/android/server/uwb/util/ArrayUtils.java
new file mode 100644
index 0000000..254e768
--- /dev/null
+++ b/service/java/com/android/server/uwb/util/ArrayUtils.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.util;
+
+import android.annotation.Nullable;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Copied over from frameworks/base/core/java/com/android/internal/util/ArrayUtils.java
+ */
+public class ArrayUtils {
+    private ArrayUtils() { /* cannot be instantiated */ }
+
+    /**
+     * Return first index of {@code value} in {@code array}, or {@code -1} if
+     * not found.
+     */
+    public static <T> int indexOf(@Nullable T[] array, T value) {
+        if (array == null) return -1;
+        for (int i = 0; i < array.length; i++) {
+            if (Objects.equals(array[i], value)) return i;
+        }
+        return -1;
+    }
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static boolean isEmpty(@Nullable int[] array) {
+        return array == null || array.length == 0;
+    }
+
+    /**
+     * True if the byte array is null or has length 0.
+     */
+    public static boolean isEmpty(@Nullable byte[] array) {
+        return array == null || array.length == 0;
+    }
+
+    public static short[] toPrimitive(List<Short> list) {
+        short[] array = new short[list.size()];
+        for (int i = 0; i < list.size(); i++) {
+            array[i] = list.get(i).shortValue();
+        }
+        return array;
+    }
+}
diff --git a/service/java/com/android/server/uwb/util/Constants.java b/service/java/com/android/server/uwb/util/Constants.java
new file mode 100644
index 0000000..04b210c
--- /dev/null
+++ b/service/java/com/android/server/uwb/util/Constants.java
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.util;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Constants used by UWB modules.
+ */
+public class Constants {
+
+    public static final byte[] FIRA_APPLET_AID =
+            new byte[] { (byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0x08,
+                    (byte) 0x67, (byte) 0x46, (byte) 0x41, (byte) 0x50, (byte) 0x00 };
+
+    /**
+     * The UWB session type
+     */
+    @IntDef(prefix = {"UWB_SESSION_TYPE_"}, value = {
+            UWB_SESSION_TYPE_UNICAST,
+            UWB_SESSION_TYPE_MULTICAST,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface UwbSessionType {
+    }
+
+    /**
+     * Unicast UWB session (1 controller, 1 controllee).
+     */
+    public static final int UWB_SESSION_TYPE_UNICAST = 0;
+    /**
+     * Multicast UWB session (1 controller, multiple controllees).
+     */
+    public static final int UWB_SESSION_TYPE_MULTICAST = 1;
+
+    private Constants() {
+    }
+}
diff --git a/service/java/com/android/server/uwb/util/DataTypeConversionUtil.java b/service/java/com/android/server/uwb/util/DataTypeConversionUtil.java
new file mode 100644
index 0000000..b39dd65
--- /dev/null
+++ b/service/java/com/android/server/uwb/util/DataTypeConversionUtil.java
@@ -0,0 +1,131 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/** Utility class for doing conversions, including bytes, hex strings, ints, and ASCII. */
+public class DataTypeConversionUtil {
+
+    private static final char[] HEX_ARRAY = {
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+    };
+
+    /**
+     * Conver the hex string to byte array.
+     */
+    public static byte[] hexStringToByteArray(String hex) {
+        // remove whitespace in the hex string.
+        hex = hex.replaceAll("\\s", "");
+
+        int len = hex.length();
+        if (len % 2 != 0) {
+            // Pad the hex string with a leading zero.
+            hex = String.format("0%s", hex);
+            len++;
+        }
+        byte[] data = new byte[len / 2];
+        for (int i = 0; i < len; i += 2) {
+            data[i / 2] =
+                    (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+                            | Character.digit(hex.charAt(i + 1), 16));
+        }
+        return data;
+    }
+
+    /**
+     * Convert the byte array to hex string.
+     */
+    @NonNull
+    public static String byteArrayToHexString(@Nullable byte[] response) {
+        if (response == null) {
+            return "";
+        }
+        return byteArrayToHexString(response, 0, response.length);
+    }
+
+    /**
+     * Convertt part of the byte array to hex string.
+     */
+    public static String byteArrayToHexString(
+            byte[] response, int startIndex, int endIndex) {
+        char[] hex = new char[(endIndex - startIndex) * 2];
+        int v;
+        for (int i = 0; i < endIndex - startIndex; i++) {
+            v = unsignedByteToInt(response[startIndex + i]);
+            hex[i * 2] = HEX_ARRAY[v >> 4];
+            hex[i * 2 + 1] = HEX_ARRAY[v & 0x0F];
+        }
+        return new String(hex);
+    }
+
+    /**
+     * Convert the byte to int.
+     */
+    public static int unsignedByteToInt(byte b) {
+        return b & 0xFF;
+    }
+
+    /**
+     * Convert the int to byte.
+     */
+    public static byte unsignedIntToByte(int n) {
+        return (byte) (n & 0xFF);
+    }
+
+    /**
+     * Convert the byte array to int using big endian.
+     */
+    public static int byteArrayToI32(byte[] bytes) {
+        if (bytes.length != 4) {
+            throw new NumberFormatException("Expected length 4 but was " + bytes.length);
+        }
+        return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt();
+    }
+
+    /**
+     * Convert the byte array with arbitrary size less than 5 to int using big endian.
+     */
+    public static int arbitraryByteArrayToI32(byte[] bytes) {
+        if (bytes.length > 4 || bytes.length < 1) {
+            throw new NumberFormatException("Expected length less than 4 but was " + bytes.length);
+        }
+        ByteBuffer byteBuffer = ByteBuffer.allocate(Integer.BYTES);
+        byteBuffer.position(Integer.BYTES - bytes.length);
+        byteBuffer.put(bytes).rewind();
+        return byteBuffer.getInt();
+    }
+
+    /**
+     * Convert the int to byte array using big endian.
+     */
+    public static byte[] i32ToByteArray(int n) {
+        return ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.BIG_ENDIAN).putInt(n).array();
+    }
+
+    /**
+     * Convert the int to byte array using little endian.
+     */
+    public static byte[] i32ToLeByteArray(int n) {
+        return ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(n).array();
+    }
+
+    private DataTypeConversionUtil() {}
+}
diff --git a/service/java/com/android/server/uwb/util/FileUtils.java b/service/java/com/android/server/uwb/util/FileUtils.java
new file mode 100644
index 0000000..0345033
--- /dev/null
+++ b/service/java/com/android/server/uwb/util/FileUtils.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.util;
+
+import android.util.AtomicFile;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Utils created for working with {@link AtomicFile}.
+ */
+public final class FileUtils {
+    private FileUtils() {}
+
+    /**
+     * Read raw data from the atomic file.
+     * Note: This is a copy of {@link AtomicFile#readFully()} modified to use the passed in
+     * {@link InputStream} which was returned using {@link AtomicFile#openRead()}.
+     */
+    public static byte[] readFromAtomicFile(AtomicFile file) throws IOException {
+        FileInputStream stream = null;
+        try {
+            stream = file.openRead();
+            int pos = 0;
+            int avail = stream.available();
+            byte[] data = new byte[avail];
+            while (true) {
+                int amt = stream.read(data, pos, data.length - pos);
+                if (amt <= 0) {
+                    return data;
+                }
+                pos += amt;
+                avail = stream.available();
+                if (avail > data.length - pos) {
+                    byte[] newData = new byte[pos + avail];
+                    System.arraycopy(data, 0, newData, 0, pos);
+                    data = newData;
+                }
+            }
+        } finally {
+            if (stream != null) stream.close();
+        }
+    }
+
+    /**
+     * Write the raw data to the atomic file.
+     */
+    public static void writeToAtomicFile(AtomicFile file, byte[] data) throws IOException {
+        // Write the data to the atomic file.
+        FileOutputStream out = null;
+        try {
+            out = file.startWrite();
+            out.write(data);
+            file.finishWrite(out);
+        } catch (IOException e) {
+            if (out != null) {
+                file.failWrite(out);
+            }
+            throw e;
+        }
+    }
+}
diff --git a/service/java/com/android/server/uwb/util/Hex.java b/service/java/com/android/server/uwb/util/Hex.java
new file mode 100644
index 0000000..518529e
--- /dev/null
+++ b/service/java/com/android/server/uwb/util/Hex.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.util;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Utility class for converting hex strings to and from byte arrays.
+ *
+ * <p>We can't use org.apache.commons.codec.binary.Hex because Android already hides it as part of
+ * /system/frameworks/ext.jar
+ *
+ * <p>Unlike standard org.apache.commons.codec.binary.Hex we allow strings with odd length to be
+ * decoded, in order to accommodate unusual partner decisions.
+ */
+public class Hex {
+    /** Base-16 encoding/decoding. */
+    @VisibleForTesting static final int RADIX = 16;
+
+    /** Upper-case encoding. */
+    @VisibleForTesting
+    static final char[] UPPER = {
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
+    };
+
+    /** Lower-case encoding. */
+    @VisibleForTesting
+    static final char[] LOWER = {
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
+    };
+
+    /** No instances. */
+    private Hex() {}
+
+    /** Returns a lower-case hex string encoding of the given byte array. */
+    public static String encode(byte[] bytes) {
+        return doEncode(bytes, LOWER);
+    }
+
+    /** Returns an upper-case hex string encoding of the given byte array. */
+    public static String encodeUpper(byte[] bytes) {
+        return doEncode(bytes, UPPER);
+    }
+
+    /**
+     * Decode the hex string to byte array.
+     */
+    public static byte[] decode(String s) throws IllegalArgumentException {
+        if (s.length() % 2 != 0) {
+            s = "0" + s;
+        }
+        return decodeEven(s);
+    }
+
+    /**
+     * Returns the byte array represented by the given string. Can handle both upper- and lower-case
+     * ASCII characters.
+     *
+     * @throws IllegalArgumentException if the string is not of even length, or if it contains a
+     *     non-hexadecimal character.
+     */
+    private static byte[] decodeEven(String s) throws IllegalArgumentException {
+        int length = s.length();
+        Preconditions.checkArgument(length % 2 == 0, "String not of even length: %s", s);
+        byte[] result = new byte[length / 2];
+        int resultPos = 0;
+        for (int pos = 0; pos < length; pos += 2) {
+            char c0 = s.charAt(pos);
+            char c1 = s.charAt(pos + 1);
+            int n0 = Character.digit(c0, RADIX);
+            int n1 = Character.digit(c1, RADIX);
+            Preconditions.checkArgument(n0 != -1, "Invalid character: '%s'", String.valueOf(c0));
+            Preconditions.checkArgument(n1 != -1, "Invalid character: '%s'", String.valueOf(c1));
+            result[resultPos++] = (byte) (n0 << 4 | n1);
+        }
+        return result;
+    }
+
+    /** Returns a hex string encoding of the given byte array using the given alphabet. */
+    private static String doEncode(byte[] bytes, char[] alphabet) {
+        StringBuilder sb = new StringBuilder(bytes.length * 2);
+        for (byte b : bytes) {
+            sb.append(alphabet[(b & 0xF0) >> 4]).append(alphabet[b & 0x0F]);
+        }
+        return sb.toString();
+    }
+}
diff --git a/service/java/com/android/server/uwb/util/ObjectIdentifier.java b/service/java/com/android/server/uwb/util/ObjectIdentifier.java
new file mode 100644
index 0000000..257738f
--- /dev/null
+++ b/service/java/com/android/server/uwb/util/ObjectIdentifier.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Arrays;
+
+// TODO: encode/decode the ObjectIdentifier per X.660.
+/**
+ * ObjectIdentifier for ADF OID.
+ */
+public class ObjectIdentifier {
+    public final byte[] value;
+
+    private ObjectIdentifier(@NonNull byte[] value) {
+        this.value = value;
+    }
+
+    /**
+     * Convert the byte array to ObjectIdentifier.
+     */
+    public static ObjectIdentifier fromBytes(@NonNull byte[] bytes) {
+        return new ObjectIdentifier(bytes);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object that) {
+        if (this == that) {
+            return true;
+        }
+        if (that == null || !(that instanceof ObjectIdentifier)) {
+            return false;
+        }
+
+        return Arrays.equals(value, ((ObjectIdentifier) that).value);
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(value);
+    }
+}
diff --git a/service/java/com/android/server/uwb/util/UwbUtil.java b/service/java/com/android/server/uwb/util/UwbUtil.java
new file mode 100755
index 0000000..96a9021
--- /dev/null
+++ b/service/java/com/android/server/uwb/util/UwbUtil.java
@@ -0,0 +1,136 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.util;
+// TODO: deprecated UwbUtil, consider to use com.android.server.uwb.util.Hex
+// and com.android.server.uwb.util.DataTypeConversionUtil
+public final class UwbUtil {
+    private static final char[] HEXCHARS = {'0', '1', '2', '3', '4', '5', '6', '7',
+            '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
+
+    public static String toHexString(byte b) {
+        StringBuffer sb = new StringBuffer(2);
+        sb.append(HEXCHARS[(b >> 4) & 0xF]);
+        sb.append(HEXCHARS[b & 0xF]);
+        return sb.toString();
+    }
+
+    public static String toHexString(byte[] data) {
+        StringBuffer sb = new StringBuffer();
+        if (data == null) {
+            return null;
+        }
+        for (int i = 0; i != data.length; i++) {
+            int b = data[i] & 0xff;
+            sb.append(HEXCHARS[(b >> 4) & 0xF]);
+            sb.append(HEXCHARS[b & 0xF]);
+        }
+        return sb.toString();
+    }
+
+    public static String toHexString(int var) {
+        byte[] byteArray = new byte[4];
+        byteArray[0] = (byte) (var & 0xff);
+        byteArray[1] = (byte) ((var >> 8) & 0xff);
+        byteArray[2] = (byte) ((var >> 16) & 0xff);
+        byteArray[3] = (byte) ((var >> 24) & 0xff);
+        StringBuilder sb = new StringBuilder();
+        for (byte b : byteArray) {
+            sb.append(HEXCHARS[(b >> 4) & 0xF]);
+            sb.append(HEXCHARS[b & 0xF]);
+        }
+        return sb.toString();
+    }
+
+    public static byte[] getByteArray(String valueString) {
+        int len = valueString.length();
+        byte[] data = new byte[len / 2];
+        for (int i = 0; i < len; i += 2) {
+            data[i / 2] = (byte) ((Character.digit(valueString.charAt(i), 16) << 4)
+                    + Character.digit(valueString.charAt(i + 1), 16));
+        }
+        return data;
+    }
+
+    public static float degreeToRadian(float angleInDegrees) {
+        return (float) ((angleInDegrees) * Math.PI / 180.0);
+    }
+
+    /**
+     * Fixed point Q format to float conversion. In Q format  Fixed point integer,
+     * integer and fractional bits are specified together.
+     * Q10.6 format = > 10 bits integer and 6 bits fractional
+     *
+     * @param qIn    Integer in Qformat
+     * @param nInts  number of integer bits
+     * @param nFracs number of fractional bits
+     * @return converted float value
+     */
+    public static float convertQFormatToFloat(int qIn, int nInts, int nFracs) {
+        int intPart = (qIn >> nFracs); // extract integer part
+        double fracPart = qIn & ((1 << nFracs) - 1); //extract fractional part
+        fracPart = Math.pow(2, -nFracs) * fracPart; //convert fractional bits to float
+        return (float) ((float) intPart + fracPart);
+    }
+
+    public static float toSignedFloat(int nInput, int nBits, int nDivider) {
+        float value = 0;
+        if (nDivider > 0) {
+            value = (float) (nInput - nBits) / nDivider;
+        } else {
+            value = (float) nInput;
+        }
+        return value;
+    }
+
+    /**
+     * Get Two's complement of a number for signed conversion
+     *
+     * @param nInput Integer
+     * @param nBits  number of bits in number
+     * @return two complement of given number value
+     */
+    public static int twos_compliment(int nInput, int nBits) {
+        if ((nInput & (1 << (nBits - 1))) != 0)  { // if sign bit is set, Eg- nInput=1111, nBits=4
+            nInput -= 1 << nBits;                  // compute negative value ,0b1111-0b10000= -1
+        }
+        return nInput;                             // return positive value as is
+    }
+
+    /**
+     * Fixed point float to Q format conversion. In Q format Fixed point integer,
+     * integer and fractional bits are specified together.
+     * Q10.6 format = > 10 bits integer and 6 bits fractional
+     *
+     * @param in     signed Float
+     * @param nInts  number of integer bits
+     * @param nFracs number of fractional bits
+     * @return converted Q format value
+     */
+    public static int convertFloatToQFormat(float in, int nInts, int nFracs) {
+        int qInt, qFracs, inputStream;
+        if (in >= 0) {
+            qInt = (int) in;
+            qFracs = (int) ((in - qInt) * (1 << (nFracs)));
+            inputStream = (qInt << nFracs) + qFracs;
+        } else {
+            qInt = (int) Math.floor(in);
+            qFracs = (int) ((in - qInt) * (1 << (nFracs)));
+            inputStream = (((1 << (nInts + 1)) + qInt) << nFracs) + qFracs;
+        }
+
+        return inputStream;
+    }
+}
diff --git a/service/lint-baseline-pre-jarjar.xml b/service/lint-baseline-pre-jarjar.xml
new file mode 100644
index 0000000..05772d7
--- /dev/null
+++ b/service/lint-baseline-pre-jarjar.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+</issues>
diff --git a/service/multichip-parser/Android.bp b/service/multichip-parser/Android.bp
new file mode 100644
index 0000000..6646160
--- /dev/null
+++ b/service/multichip-parser/Android.bp
@@ -0,0 +1,24 @@
+//
+// 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.
+//
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+xsd_config {
+    name: "uwb_config",
+    srcs: ["uwbConfig.xsd"],
+    package_name: "com.android.uwb",
+}
diff --git a/service/multichip-parser/api/current.txt b/service/multichip-parser/api/current.txt
new file mode 100644
index 0000000..d2dbc7f
--- /dev/null
+++ b/service/multichip-parser/api/current.txt
@@ -0,0 +1,44 @@
+// Signature format: 2.0
+package com.android.uwb {
+
+  public class ChipGroupInfo {
+    ctor public ChipGroupInfo();
+    method public java.util.List<com.android.uwb.ChipInfo> getChip();
+    method public String getSharedLib();
+    method public void setSharedLib(String);
+  }
+
+  public class ChipInfo {
+    ctor public ChipInfo();
+    method public String getId();
+    method public com.android.uwb.Coordinates getPosition();
+    method public void setId(String);
+    method public void setPosition(com.android.uwb.Coordinates);
+  }
+
+  public class Coordinates {
+    ctor public Coordinates();
+    method public java.math.BigDecimal getX();
+    method public java.math.BigDecimal getY();
+    method public java.math.BigDecimal getZ();
+    method public void setX(java.math.BigDecimal);
+    method public void setY(java.math.BigDecimal);
+    method public void setZ(java.math.BigDecimal);
+  }
+
+  public class UwbChipConfig {
+    ctor public UwbChipConfig();
+    method public java.util.List<com.android.uwb.ChipGroupInfo> getChipGroup();
+    method public String getDefaultChipId();
+    method public void setDefaultChipId(String);
+  }
+
+  public class XmlParser {
+    ctor public XmlParser();
+    method public static com.android.uwb.UwbChipConfig read(java.io.InputStream) throws javax.xml.datatype.DatatypeConfigurationException, java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+    method public static String readText(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+    method public static void skip(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+  }
+
+}
+
diff --git a/service/multichip-parser/api/last_current.txt b/service/multichip-parser/api/last_current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/service/multichip-parser/api/last_current.txt
diff --git a/service/multichip-parser/api/last_removed.txt b/service/multichip-parser/api/last_removed.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/service/multichip-parser/api/last_removed.txt
diff --git a/service/multichip-parser/api/removed.txt b/service/multichip-parser/api/removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/service/multichip-parser/api/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/service/multichip-parser/uwbConfig.xsd b/service/multichip-parser/uwbConfig.xsd
new file mode 100644
index 0000000..586a2e0
--- /dev/null
+++ b/service/multichip-parser/uwbConfig.xsd
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+         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.
+-->
+<xsd:schema version="2.0"
+           elementFormDefault="qualified"
+           xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+    <xsd:element name="uwbChipConfig">
+        <xsd:annotation>
+            <xsd:documentation>
+                A collection of chipGroups.
+            </xsd:documentation>
+        </xsd:annotation>
+        <xsd:complexType>
+        <xsd:sequence>
+            <xsd:element name="defaultChipId" type="xsd:string">
+                <xsd:annotation>
+                    <xsd:documentation>
+                        The id of the UWB chip that should be used by the framework if the framework
+                        doesn't specify which chip it wants to use.
+                    </xsd:documentation>
+                </xsd:annotation>
+            </xsd:element>
+            <xsd:element name="chipGroup" type="chipGroupInfo" maxOccurs="unbounded"/>
+        </xsd:sequence>
+        </xsd:complexType>
+    </xsd:element>
+
+    <xsd:complexType name="chipGroupInfo">
+        <xsd:annotation>
+            <xsd:documentation>
+                A collection of UWB chips that are connected to the AP via a common hardware
+                connection and a common shared library.
+            </xsd:documentation>
+        </xsd:annotation>
+        <xsd:sequence>
+            <xsd:element name="sharedLib" type="xsd:string"/>
+            <xsd:element name="chip" type="chipInfo" maxOccurs="unbounded"/>
+        </xsd:sequence>
+    </xsd:complexType>
+
+    <xsd:complexType name="chipInfo">
+        <xsd:annotation>
+            <xsd:documentation>
+                A single UWB chip defined by its id and position.
+
+                Even a single UWB chip must be part of a chipGroup.
+            </xsd:documentation>
+        </xsd:annotation>
+        <xsd:sequence>
+            <xsd:element name="id" type="xsd:string"/>
+            <xsd:element name="position" type="coordinates" minOccurs="0"/>
+        </xsd:sequence>
+    </xsd:complexType>
+
+    <xsd:complexType name="coordinates">
+        <xsd:annotation>
+            <xsd:documentation>
+                The physical 3D position of the UWB antenna, measured in meters from the origin of
+                coordinate system that the device uses.
+            </xsd:documentation>
+        </xsd:annotation>
+        <xsd:sequence>
+            <xsd:element name="x" type="xsd:decimal"/>
+            <xsd:element name="y" type="xsd:decimal"/>
+            <xsd:element name="z" type="xsd:decimal"/>
+        </xsd:sequence>
+    </xsd:complexType>
+</xsd:schema>
\ No newline at end of file
diff --git a/service/proguard.flags b/service/proguard.flags
new file mode 100644
index 0000000..6b6ebb5
--- /dev/null
+++ b/service/proguard.flags
@@ -0,0 +1,2 @@
+# Prevent proguard from stripping out any service-uwb.
+-keep class com.android.server.uwb.** { *; }
diff --git a/service/support_lib/Android.bp b/service/support_lib/Android.bp
new file mode 100644
index 0000000..3b457e1
--- /dev/null
+++ b/service/support_lib/Android.bp
@@ -0,0 +1,115 @@
+//
+// 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.
+//
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+    name: "support-lib-uwb-common-defaults",
+    defaults: ["uwb-module-sdk-version-defaults"],
+    sdk_version: "module_Tiramisu",
+    libs: [
+        "framework-annotations-lib",
+        "framework-uwb-pre-jarjar",
+    ],
+    apex_available: [
+        "com.android.uwb",
+    ],
+    visibility : [
+        "//cts/tests/uwb",
+        "//cts/hostsidetests/multidevices/uwb/snippet",
+        "//external/sl4a/Common",
+        "//packages/modules/Uwb/service:__subpackages__",
+    ]
+}
+
+java_library {
+    name: "com.uwb.support.base",
+    defaults: ["support-lib-uwb-common-defaults"],
+    srcs: [
+        "src/com/google/uwb/support/base/**/*.java",
+    ],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "modules-utils-preconditions",
+    ],
+}
+
+java_library {
+    name: "com.uwb.support.ccc",
+    defaults: ["support-lib-uwb-common-defaults"],
+    srcs: [
+        "src/com/google/uwb/support/ccc/**/*.java",
+    ],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "com.uwb.support.base",
+        "modules-utils-preconditions",
+    ],
+}
+
+java_library {
+    name: "com.uwb.support.fira",
+    defaults: ["support-lib-uwb-common-defaults"],
+    srcs: [
+        "src/com/google/uwb/support/fira/**/*.java",
+    ],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "com.uwb.support.base",
+        "modules-utils-preconditions",
+    ],
+}
+
+java_library {
+    name: "com.uwb.support.generic",
+    defaults: ["support-lib-uwb-common-defaults"],
+    srcs: [
+        "src/com/google/uwb/support/generic/**/*.java",
+    ],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "com.uwb.support.base",
+        "com.uwb.support.ccc",
+        "com.uwb.support.fira",
+        "modules-utils-preconditions",
+    ],
+}
+
+java_library {
+    name: "com.uwb.support.multichip",
+    defaults: ["support-lib-uwb-common-defaults"],
+    srcs: [
+        "src/com/google/uwb/support/multichip/**/*.java",
+    ],
+    static_libs: [
+    ],
+    visibility: ["//cts/tests/uwb"],
+}
+
+java_library {
+    name: "com.uwb.support.profile",
+    defaults: ["support-lib-uwb-common-defaults"],
+    srcs: [
+        "src/com/google/uwb/support/profile/**/*.java",
+    ],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "com.uwb.support.base",
+        "com.uwb.support.fira",
+        "modules-utils-preconditions",
+    ],
+}
diff --git a/service/support_lib/src/com/google/uwb/support/base/FlagEnum.java b/service/support_lib/src/com/google/uwb/support/base/FlagEnum.java
new file mode 100644
index 0000000..ccd51de
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/base/FlagEnum.java
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.base;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Flags backed by long value.
+ * If all the enum values fit in a integer, you can use
+ * {@link #toInt(Set)} and {@link #toEnumSet(int, Enum[])} safely. Otherwise, those methods will
+ * throw an {@link ArithmeticException} on overflow.
+ */
+public interface FlagEnum {
+    long getValue();
+
+    static <E extends Enum<E> & FlagEnum> int toInt(Set<E> enumSet) {
+        int value = 0;
+        for (E flag : enumSet) {
+            value |= Math.toIntExact(flag.getValue());
+        }
+        return value;
+    }
+
+    static <E extends Enum<E> & FlagEnum> EnumSet<E> toEnumSet(int flags, E[] values) {
+        if (values.length == 0) {
+            throw new IllegalArgumentException("Empty FlagEnum");
+        }
+        List<E> flagList = new ArrayList<>();
+        for (E value : values) {
+            if ((flags & Math.toIntExact(value.getValue())) != 0) {
+                flagList.add(value);
+            }
+        }
+        if (flagList.isEmpty()) {
+            Class<E> c = values[0].getDeclaringClass();
+            return EnumSet.noneOf(c);
+        } else {
+            return EnumSet.copyOf(flagList);
+        }
+    }
+
+    static <E extends Enum<E> & FlagEnum> long toLong(Set<E> enumSet) {
+        long value = 0;
+        for (E flag : enumSet) {
+            value |= flag.getValue();
+        }
+        return value;
+    }
+
+    static <E extends Enum<E> & FlagEnum> EnumSet<E> longToEnumSet(long flags, E[] values) {
+        if (values.length == 0) {
+            throw new IllegalArgumentException("Empty FlagEnum");
+        }
+        List<E> flagList = new ArrayList<>();
+        for (E value : values) {
+            if ((flags & value.getValue()) != 0) {
+                flagList.add(value);
+            }
+        }
+        if (flagList.isEmpty()) {
+            Class<E> c = values[0].getDeclaringClass();
+            return EnumSet.noneOf(c);
+        } else {
+            return EnumSet.copyOf(flagList);
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/base/Params.java b/service/support_lib/src/com/google/uwb/support/base/Params.java
new file mode 100644
index 0000000..847b34d
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/base/Params.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.base;
+
+import android.os.Build.VERSION_CODES;
+import android.os.PersistableBundle;
+
+import androidx.annotation.RequiresApi;
+
+/** Provides common parameter operations. */
+@RequiresApi(VERSION_CODES.LOLLIPOP)
+public abstract class Params {
+    private static final String KEY_BUNDLE_VERSION = "bundle_version";
+    protected static final int BUNDLE_VERSION_UNKNOWN = -1;
+
+    protected static final String KEY_PROTOCOL_NAME = "protocol_name";
+    protected static final String PROTOCOL_NAME_UNKNOWN = "unknown";
+
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putInt(KEY_BUNDLE_VERSION, getBundleVersion());
+        bundle.putString(KEY_PROTOCOL_NAME, getProtocolName());
+        return bundle;
+    }
+
+    public abstract String getProtocolName();
+
+    protected abstract int getBundleVersion();
+
+    public static int getBundleVersion(PersistableBundle bundle) {
+        return bundle.getInt(KEY_BUNDLE_VERSION, BUNDLE_VERSION_UNKNOWN);
+    }
+
+    public static boolean isProtocol(PersistableBundle bundle, String protocol) {
+        return bundle.getString(KEY_PROTOCOL_NAME, PROTOCOL_NAME_UNKNOWN).equals(protocol);
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/base/ProtocolVersion.java b/service/support_lib/src/com/google/uwb/support/base/ProtocolVersion.java
new file mode 100644
index 0000000..edc2420
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/base/ProtocolVersion.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.base;
+
+import android.os.Parcel;
+
+import androidx.annotation.Nullable;
+
+import java.util.Arrays;
+
+/** Provides parameter versioning. */
+public class ProtocolVersion {
+    public static ProtocolVersion fromString(String protocol) {
+        String[] parts = protocol.split("\\.", -1);
+        if (parts.length != 2) {
+            throw new IllegalArgumentException("Invalid protocol version: " + protocol);
+        }
+
+        return new ProtocolVersion(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
+    }
+
+    private final int mMajor;
+    private final int mMinor;
+
+    public ProtocolVersion(int major, int minor) {
+        mMajor = major;
+        mMinor = minor;
+    }
+
+    protected ProtocolVersion(Parcel in) {
+        mMajor = in.readInt();
+        mMinor = in.readInt();
+    }
+
+    @Override
+    public String toString() {
+        return mMajor + "." + mMinor;
+    }
+
+    public int getMajor() {
+        return mMajor;
+    }
+
+    public int getMinor() {
+        return mMinor;
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(new int[] {mMajor, mMinor});
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (other instanceof ProtocolVersion) {
+            ProtocolVersion otherProtocol = (ProtocolVersion) other;
+            return otherProtocol.mMajor == mMajor && otherProtocol.mMinor == mMinor;
+        }
+        return false;
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/base/RequiredParam.java b/service/support_lib/src/com/google/uwb/support/base/RequiredParam.java
new file mode 100644
index 0000000..f1e6657
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/base/RequiredParam.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.base;
+
+/** Provides required parameter enforcement */
+public class RequiredParam<T> {
+    private T mValue;
+    private boolean mIsSet = false;
+
+    public void set(T value) {
+        mValue = value;
+        mIsSet = true;
+    }
+
+    public T get() {
+        if (!mIsSet) {
+            throw new IllegalStateException("Required Parameter not set");
+        }
+        return mValue;
+    }
+
+    public boolean isSet() {
+        return mIsSet;
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/ccc/CccOpenRangingParams.java b/service/support_lib/src/com/google/uwb/support/ccc/CccOpenRangingParams.java
new file mode 100644
index 0000000..9ddf074
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/ccc/CccOpenRangingParams.java
@@ -0,0 +1,317 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.ccc;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.NonNull;
+import android.os.Build.VERSION_CODES;
+import android.os.PersistableBundle;
+import android.uwb.UwbManager;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.RequiresApi;
+
+import com.google.uwb.support.base.RequiredParam;
+
+/**
+ * Defines parameters for CCC open operation
+ *
+ * <p>This is passed as a bundle to the service API {@link UwbManager#openRangingSession}.
+ */
+@RequiresApi(VERSION_CODES.LOLLIPOP)
+public class CccOpenRangingParams extends CccParams {
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    private static final String KEY_PROTOCOL_VERSION = "protocol_version";
+    private static final String KEY_UWB_CONFIG = "uwb_config";
+    private static final String KEY_PULSE_SHAPE_COMBO = "pulse_shape_combo";
+    private static final String KEY_SESSION_ID = "session_id";
+    private static final String KEY_RAN_MULTIPLIER = "ran_multiplier";
+    private static final String KEY_CHANNEL = "channel";
+    private static final String KEY_NUM_CHAPS_PER_SLOT = "num_chaps_per_slot";
+    private static final String KEY_NUM_RESPONDER_NODES = "num_responder_nodes";
+    private static final String KEY_NUM_SLOTS_PER_ROUND = "num_slots_per_round";
+    private static final String KEY_SYNC_CODE_INDEX = "sync_code_index";
+    private static final String KEY_HOPPING_CONFIG_MODE = "hopping_config_mode";
+    private static final String KEY_HOPPING_SEQUENCE = "hopping_sequence";
+
+    private final CccProtocolVersion mProtocolVersion;
+    @UwbConfig private final int mUwbConfig;
+    private final CccPulseShapeCombo mPulseShapeCombo;
+    private final int mSessionId;
+    private final int mRanMultiplier;
+    @Channel private final int mChannel;
+    private final int mNumChapsPerSlot;
+    private final int mNumResponderNodes;
+    private final int mNumSlotsPerRound;
+    @SyncCodeIndex private final int mSyncCodeIndex;
+    @HoppingConfigMode private final int mHoppingConfigMode;
+    @HoppingSequence private final int mHoppingSequence;
+
+    private CccOpenRangingParams(
+            CccProtocolVersion protocolVersion,
+            @UwbConfig int uwbConfig,
+            CccPulseShapeCombo pulseShapeCombo,
+            int sessionId,
+            int ranMultiplier,
+            @Channel int channel,
+            int numChapsPerSlot,
+            int numResponderNodes,
+            int numSlotsPerRound,
+            @SyncCodeIndex int syncCodeIndex,
+            @HoppingConfigMode int hoppingConfigMode,
+            @HoppingSequence int hoppingSequence) {
+        mProtocolVersion = protocolVersion;
+        mUwbConfig = uwbConfig;
+        mPulseShapeCombo = pulseShapeCombo;
+        mSessionId = sessionId;
+        mRanMultiplier = ranMultiplier;
+        mChannel = channel;
+        mNumChapsPerSlot = numChapsPerSlot;
+        mNumResponderNodes = numResponderNodes;
+        mNumSlotsPerRound = numSlotsPerRound;
+        mSyncCodeIndex = syncCodeIndex;
+        mHoppingConfigMode = hoppingConfigMode;
+        mHoppingSequence = hoppingSequence;
+    }
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    @Override
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = super.toBundle();
+        bundle.putString(KEY_PROTOCOL_VERSION, mProtocolVersion.toString());
+        bundle.putInt(KEY_UWB_CONFIG, mUwbConfig);
+        bundle.putString(KEY_PULSE_SHAPE_COMBO, mPulseShapeCombo.toString());
+        bundle.putInt(KEY_SESSION_ID, mSessionId);
+        bundle.putInt(KEY_RAN_MULTIPLIER, mRanMultiplier);
+        bundle.putInt(KEY_CHANNEL, mChannel);
+        bundle.putInt(KEY_NUM_CHAPS_PER_SLOT, mNumChapsPerSlot);
+        bundle.putInt(KEY_NUM_RESPONDER_NODES, mNumResponderNodes);
+        bundle.putInt(KEY_NUM_SLOTS_PER_ROUND, mNumSlotsPerRound);
+        bundle.putInt(KEY_SYNC_CODE_INDEX, mSyncCodeIndex);
+        bundle.putInt(KEY_HOPPING_CONFIG_MODE, mHoppingConfigMode);
+        bundle.putInt(KEY_HOPPING_SEQUENCE, mHoppingSequence);
+        return bundle;
+    }
+
+    public static CccOpenRangingParams fromBundle(PersistableBundle bundle) {
+        if (!isCorrectProtocol(bundle)) {
+            throw new IllegalArgumentException("Invalid protocol");
+        }
+
+        switch (getBundleVersion(bundle)) {
+            case BUNDLE_VERSION_1:
+                return parseBundleVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("unknown bundle version");
+        }
+    }
+
+    private static CccOpenRangingParams parseBundleVersion1(PersistableBundle bundle) {
+        return new Builder()
+                .setProtocolVersion(
+                        CccProtocolVersion.fromString(
+                                checkNotNull(bundle.getString(KEY_PROTOCOL_VERSION))))
+                .setUwbConfig(bundle.getInt(KEY_UWB_CONFIG))
+                .setPulseShapeCombo(
+                        CccPulseShapeCombo.fromString(
+                                checkNotNull(bundle.getString(KEY_PULSE_SHAPE_COMBO))))
+                .setSessionId(bundle.getInt(KEY_SESSION_ID))
+                .setRanMultiplier(bundle.getInt(KEY_RAN_MULTIPLIER))
+                .setChannel(bundle.getInt(KEY_CHANNEL))
+                .setNumChapsPerSlot(bundle.getInt(KEY_NUM_CHAPS_PER_SLOT))
+                .setNumResponderNodes(bundle.getInt(KEY_NUM_RESPONDER_NODES))
+                .setNumSlotsPerRound(bundle.getInt(KEY_NUM_SLOTS_PER_ROUND))
+                .setSyncCodeIndex(bundle.getInt(KEY_SYNC_CODE_INDEX))
+                .setHoppingConfigMode(bundle.getInt(KEY_HOPPING_CONFIG_MODE))
+                .setHoppingSequence(bundle.getInt(KEY_HOPPING_SEQUENCE))
+                .build();
+    }
+
+    public CccProtocolVersion getProtocolVersion() {
+        return mProtocolVersion;
+    }
+
+    @UwbConfig
+    public int getUwbConfig() {
+        return mUwbConfig;
+    }
+
+    public CccPulseShapeCombo getPulseShapeCombo() {
+        return mPulseShapeCombo;
+    }
+
+    public int getSessionId() {
+        return mSessionId;
+    }
+
+    @IntRange(from = 0, to = 255)
+    public int getRanMultiplier() {
+        return mRanMultiplier;
+    }
+
+    @Channel
+    public int getChannel() {
+        return mChannel;
+    }
+
+    public int getNumChapsPerSlot() {
+        return mNumChapsPerSlot;
+    }
+
+    public int getNumResponderNodes() {
+        return mNumResponderNodes;
+    }
+
+    public int getNumSlotsPerRound() {
+        return mNumSlotsPerRound;
+    }
+
+    @SyncCodeIndex
+    public int getSyncCodeIndex() {
+        return mSyncCodeIndex;
+    }
+
+    @HoppingConfigMode
+    public int getHoppingConfigMode() {
+        return mHoppingConfigMode;
+    }
+
+    @HoppingSequence
+    public int getHoppingSequence() {
+        return mHoppingSequence;
+    }
+
+    /** Builder */
+    public static final class Builder {
+        private RequiredParam<CccProtocolVersion> mProtocolVersion = new RequiredParam<>();
+        @UwbConfig private RequiredParam<Integer> mUwbConfig = new RequiredParam<>();
+        private RequiredParam<CccPulseShapeCombo> mPulseShapeCombo = new RequiredParam<>();
+        private RequiredParam<Integer> mSessionId = new RequiredParam<>();
+        private RequiredParam<Integer> mRanMultiplier = new RequiredParam<>();
+        @Channel private RequiredParam<Integer> mChannel = new RequiredParam<>();
+        @ChapsPerSlot private RequiredParam<Integer> mNumChapsPerSlot = new RequiredParam<>();
+        private RequiredParam<Integer> mNumResponderNodes = new RequiredParam<>();
+        @SlotsPerRound private RequiredParam<Integer> mNumSlotsPerRound = new RequiredParam<>();
+        @SyncCodeIndex private RequiredParam<Integer> mSyncCodeIndex = new RequiredParam<>();
+
+        @HoppingConfigMode
+        private RequiredParam<Integer> mHoppingConfigMode = new RequiredParam<>();
+
+        @HoppingSequence private RequiredParam<Integer> mHoppingSequence = new RequiredParam<>();
+
+        public Builder() {}
+
+        public Builder(@NonNull Builder builder) {
+            mProtocolVersion.set(builder.mProtocolVersion.get());
+            mUwbConfig.set(builder.mUwbConfig.get());
+            mPulseShapeCombo.set(builder.mPulseShapeCombo.get());
+            mSessionId.set(builder.mSessionId.get());
+            mRanMultiplier.set(builder.mRanMultiplier.get());
+            mChannel.set(builder.mChannel.get());
+            mNumChapsPerSlot.set(builder.mNumChapsPerSlot.get());
+            mNumResponderNodes.set(builder.mNumResponderNodes.get());
+            mNumSlotsPerRound.set(builder.mNumSlotsPerRound.get());
+            mSyncCodeIndex.set(builder.mSyncCodeIndex.get());
+            mHoppingConfigMode.set(builder.mHoppingConfigMode.get());
+            mHoppingSequence.set(builder.mHoppingSequence.get());
+        }
+
+        public Builder setProtocolVersion(CccProtocolVersion version) {
+            mProtocolVersion.set(version);
+            return this;
+        }
+
+        public Builder setUwbConfig(@UwbConfig int uwbConfig) {
+            mUwbConfig.set(uwbConfig);
+            return this;
+        }
+
+        public Builder setPulseShapeCombo(CccPulseShapeCombo pulseShapeCombo) {
+            mPulseShapeCombo.set(pulseShapeCombo);
+            return this;
+        }
+
+        public Builder setSessionId(int sessionId) {
+            mSessionId.set(sessionId);
+            return this;
+        }
+
+        public Builder setRanMultiplier(int ranMultiplier) {
+            mRanMultiplier.set(ranMultiplier);
+            return this;
+        }
+
+        public Builder setChannel(@Channel int channel) {
+            mChannel.set(channel);
+            return this;
+        }
+
+        public Builder setNumChapsPerSlot(@ChapsPerSlot int numChapsPerSlot) {
+            mNumChapsPerSlot.set(numChapsPerSlot);
+            return this;
+        }
+
+        public Builder setNumResponderNodes(int numResponderNodes) {
+            mNumResponderNodes.set(numResponderNodes);
+            return this;
+        }
+
+        public Builder setNumSlotsPerRound(@SlotsPerRound int numSlotsPerRound) {
+            mNumSlotsPerRound.set(numSlotsPerRound);
+            return this;
+        }
+
+        public Builder setSyncCodeIndex(@SyncCodeIndex int syncCodeIndex) {
+            mSyncCodeIndex.set(syncCodeIndex);
+            return this;
+        }
+
+        public Builder setHoppingConfigMode(@HoppingConfigMode int hoppingConfigMode) {
+            mHoppingConfigMode.set(hoppingConfigMode);
+            return this;
+        }
+
+        public Builder setHoppingSequence(@HoppingSequence int hoppingSequence) {
+            mHoppingSequence.set(hoppingSequence);
+            return this;
+        }
+
+        public CccOpenRangingParams build() {
+            return new CccOpenRangingParams(
+                    mProtocolVersion.get(),
+                    mUwbConfig.get(),
+                    mPulseShapeCombo.get(),
+                    mSessionId.get(),
+                    mRanMultiplier.get(),
+                    mChannel.get(),
+                    mNumChapsPerSlot.get(),
+                    mNumResponderNodes.get(),
+                    mNumSlotsPerRound.get(),
+                    mSyncCodeIndex.get(),
+                    mHoppingConfigMode.get(),
+                    mHoppingSequence.get());
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/ccc/CccParams.java b/service/support_lib/src/com/google/uwb/support/ccc/CccParams.java
new file mode 100644
index 0000000..ae71878
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/ccc/CccParams.java
@@ -0,0 +1,203 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.ccc;
+
+import android.os.Build.VERSION_CODES;
+import android.os.PersistableBundle;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
+import androidx.annotation.RequiresApi;
+
+import com.google.uwb.support.base.Params;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Defines parameters for CCC operation */
+@RequiresApi(VERSION_CODES.LOLLIPOP)
+public abstract class CccParams extends Params {
+    public static final String PROTOCOL_NAME = "ccc";
+
+    @Override
+    public final String getProtocolName() {
+        return PROTOCOL_NAME;
+    }
+
+    public static boolean isCorrectProtocol(PersistableBundle bundle) {
+        return isProtocol(bundle, PROTOCOL_NAME);
+    }
+
+    public static boolean isCorrectProtocol(String protocolName) {
+        return protocolName.equals(PROTOCOL_NAME);
+    }
+
+    public static final CccProtocolVersion PROTOCOL_VERSION_1_0 = new CccProtocolVersion(1, 0);
+
+    /** Pulse Shapse (details below) */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE,
+                PULSE_SHAPE_PRECURSOR_FREE,
+                PULSE_SHAPE_PRECURSOR_FREE_SPECIAL
+            })
+    public @interface PulseShape {}
+
+    /**
+     * Indicates the symmetrical root raised cosine pulse shape as defined by Digital Key R3 Section
+     * 21.5.
+     */
+    public static final int PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE = 0x0;
+
+    /** Indicates the precursor-free pulse shape as defined by Digital Key R3 Section 21.5. */
+    public static final int PULSE_SHAPE_PRECURSOR_FREE = 0x1;
+
+    /**
+     * Indicates a special case of the precursor-free pulse shape as defined by Digital Key R3
+     * Section 21.5.
+     */
+    public static final int PULSE_SHAPE_PRECURSOR_FREE_SPECIAL = 0x2;
+
+    /** Config (details below) */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                UWB_CONFIG_0,
+                UWB_CONFIG_1,
+            })
+    public @interface UwbConfig {}
+
+    /** Indicates UWB Config 0 as defined by Digital Key R3 Section 21.4. */
+    public static final int UWB_CONFIG_0 = 0;
+
+    /** Indicates UWB Config 1 as defined by Digital Key R3 Section 21.4. */
+    public static final int UWB_CONFIG_1 = 1;
+
+    /** Channels */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                UWB_CHANNEL_5,
+                UWB_CHANNEL_9,
+            })
+    public @interface Channel {}
+
+    public static final int UWB_CHANNEL_5 = 5;
+    public static final int UWB_CHANNEL_9 = 9;
+
+    /** Sync Codes */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntRange(from = 1, to = 32)
+    public @interface SyncCodeIndex {}
+
+    /** Hopping Config */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                HOPPING_CONFIG_MODE_NONE,
+                HOPPING_CONFIG_MODE_CONTINUOUS,
+                HOPPING_CONFIG_MODE_ADAPTIVE,
+            })
+    public @interface HoppingConfigMode {}
+
+    public static final int HOPPING_CONFIG_MODE_NONE = 0;
+    public static final int HOPPING_CONFIG_MODE_CONTINUOUS = 1;
+    public static final int HOPPING_CONFIG_MODE_ADAPTIVE = 2;
+
+    /** Hopping Sequence */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                HOPPING_SEQUENCE_DEFAULT,
+                HOPPING_SEQUENCE_AES,
+            })
+    public @interface HoppingSequence {}
+
+    public static final int HOPPING_SEQUENCE_DEFAULT = 0;
+    public static final int HOPPING_SEQUENCE_AES = 1;
+
+    /** Chaps per Slot (i.e. slot duration) */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                CHAPS_PER_SLOT_3,
+                CHAPS_PER_SLOT_4,
+                CHAPS_PER_SLOT_6,
+                CHAPS_PER_SLOT_8,
+                CHAPS_PER_SLOT_9,
+                CHAPS_PER_SLOT_12,
+                CHAPS_PER_SLOT_24,
+            })
+    public @interface ChapsPerSlot {}
+
+    public static final int CHAPS_PER_SLOT_3 = 3;
+    public static final int CHAPS_PER_SLOT_4 = 4;
+    public static final int CHAPS_PER_SLOT_6 = 6;
+    public static final int CHAPS_PER_SLOT_8 = 8;
+    public static final int CHAPS_PER_SLOT_9 = 9;
+    public static final int CHAPS_PER_SLOT_12 = 12;
+    public static final int CHAPS_PER_SLOT_24 = 24;
+
+    /** Slots per Round */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                SLOTS_PER_ROUND_6,
+                SLOTS_PER_ROUND_8,
+                SLOTS_PER_ROUND_9,
+                SLOTS_PER_ROUND_12,
+                SLOTS_PER_ROUND_16,
+                SLOTS_PER_ROUND_18,
+                SLOTS_PER_ROUND_24,
+                SLOTS_PER_ROUND_32,
+                SLOTS_PER_ROUND_36,
+                SLOTS_PER_ROUND_48,
+                SLOTS_PER_ROUND_72,
+                SLOTS_PER_ROUND_96,
+            })
+    public @interface SlotsPerRound {}
+
+    public static final int SLOTS_PER_ROUND_6 = 6;
+    public static final int SLOTS_PER_ROUND_8 = 8;
+    public static final int SLOTS_PER_ROUND_9 = 9;
+    public static final int SLOTS_PER_ROUND_12 = 12;
+    public static final int SLOTS_PER_ROUND_16 = 16;
+    public static final int SLOTS_PER_ROUND_18 = 18;
+    public static final int SLOTS_PER_ROUND_24 = 24;
+    public static final int SLOTS_PER_ROUND_32 = 32;
+    public static final int SLOTS_PER_ROUND_36 = 36;
+    public static final int SLOTS_PER_ROUND_48 = 48;
+    public static final int SLOTS_PER_ROUND_72 = 72;
+    public static final int SLOTS_PER_ROUND_96 = 96;
+
+    /** Error Reason */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                PROTOCOL_ERROR_UNKNOWN,
+                PROTOCOL_ERROR_SE_BUSY,
+                PROTOCOL_ERROR_LIFECYCLE,
+                PROTOCOL_ERROR_NOT_FOUND,
+            })
+    public @interface ProtocolError {}
+
+    public static final int PROTOCOL_ERROR_UNKNOWN = 0;
+    public static final int PROTOCOL_ERROR_SE_BUSY = 1;
+    public static final int PROTOCOL_ERROR_LIFECYCLE = 2;
+    public static final int PROTOCOL_ERROR_NOT_FOUND = 3;
+}
diff --git a/service/support_lib/src/com/google/uwb/support/ccc/CccProtocolVersion.java b/service/support_lib/src/com/google/uwb/support/ccc/CccProtocolVersion.java
new file mode 100644
index 0000000..9dd0720
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/ccc/CccProtocolVersion.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.ccc;
+
+import com.google.uwb.support.base.ProtocolVersion;
+
+import java.nio.ByteBuffer;
+
+/** Provides parameter versioning for CCC. */
+public class CccProtocolVersion extends ProtocolVersion {
+    private static final int CCC_PACKED_BYTE_COUNT = 2;
+
+    public CccProtocolVersion(int major, int minor) {
+        super(major, minor);
+    }
+
+    public static CccProtocolVersion fromString(String protocol) {
+        String[] parts = protocol.split("\\.", -1);
+        if (parts.length != 2) {
+            throw new IllegalArgumentException("Invalid protocol version: " + protocol);
+        }
+
+        return new CccProtocolVersion(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
+    }
+
+    public byte[] toBytes() {
+        return ByteBuffer.allocate(bytesUsed())
+                .put((byte) getMajor())
+                .put((byte) getMinor())
+                .array();
+    }
+
+    public static CccProtocolVersion fromBytes(byte[] data, int startIndex) {
+        int major = data[startIndex];
+        int minor = data[startIndex + 1];
+        return new CccProtocolVersion(major, minor);
+    }
+
+    public static int bytesUsed() {
+        return CCC_PACKED_BYTE_COUNT;
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/ccc/CccPulseShapeCombo.java b/service/support_lib/src/com/google/uwb/support/ccc/CccPulseShapeCombo.java
new file mode 100644
index 0000000..078a9ae
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/ccc/CccPulseShapeCombo.java
@@ -0,0 +1,136 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.ccc;
+
+import android.os.Build.VERSION_CODES;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/** Defines parameters for CCC pulse shape combo object */
+@RequiresApi(VERSION_CODES.LOLLIPOP)
+public class CccPulseShapeCombo extends CccParams {
+    private static final int CCC_PACKED_BYTE_COUNT = 1;
+
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    private static final String KEY_INITIATOR_TX = "initiator_tx";
+    private static final String KEY_RESPONDER_TX = "responder_tx";
+
+    @PulseShape private final int mInitiatorTx;
+    @PulseShape private final int mResponderTx;
+
+    public CccPulseShapeCombo(@PulseShape int initiatorTx, @PulseShape int responderTx) {
+        mInitiatorTx = initiatorTx;
+        mResponderTx = responderTx;
+    }
+
+    public static int bytesUsed() {
+        return CCC_PACKED_BYTE_COUNT;
+    }
+
+    public static CccPulseShapeCombo fromBytes(byte[] data, int startIndex) {
+        byte initiatorTx = (byte) ((data[startIndex] >> 4) & 0x0F);
+        byte responderTx = (byte) (data[startIndex] & 0x0F);
+        return new CccPulseShapeCombo(initiatorTx, responderTx);
+    }
+
+    public byte[] toBytes() {
+        byte pulseShapeCombo = (byte) (mInitiatorTx << 4 | mResponderTx);
+        return ByteBuffer.allocate(bytesUsed()).put((byte) pulseShapeCombo).array();
+    }
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    public String toString() {
+        return getBundleVersion()
+                + "."
+                + getProtocolName()
+                + "."
+                + mInitiatorTx
+                + "."
+                + mResponderTx;
+    }
+
+    public static CccPulseShapeCombo fromString(String cccPulseShapeCombo) {
+        String[] parts = cccPulseShapeCombo.split("\\.", -1);
+        if (parts.length == 0) {
+            throw new IllegalArgumentException("Invalid pulse shape combo: " + cccPulseShapeCombo);
+        }
+
+        int bundleVersion = Integer.parseInt(parts[0]);
+
+        switch (bundleVersion) {
+            case BUNDLE_VERSION_1:
+                return parseBundleVersion1(cccPulseShapeCombo);
+
+            default:
+                throw new IllegalArgumentException("unknown bundle version");
+        }
+    }
+
+    private static CccPulseShapeCombo parseBundleVersion1(String cccPulseShapeCombo) {
+        String[] parts = cccPulseShapeCombo.split("\\.", -1);
+        if (parts.length != 4) {
+            throw new IllegalArgumentException(
+                    "Invalid version 1 pulse shape combo: " + cccPulseShapeCombo);
+        }
+
+        String protocolName = parts[1];
+
+        if (!isCorrectProtocol(protocolName)) {
+            throw new IllegalArgumentException("Invalid protocol");
+        }
+
+        @PulseShape int initiatorTx = Integer.parseInt(parts[2]);
+        @PulseShape int responderTx = Integer.parseInt(parts[3]);
+
+        return new CccPulseShapeCombo(initiatorTx, responderTx);
+    }
+
+    @PulseShape
+    public int getInitiatorTx() {
+        return mInitiatorTx;
+    }
+
+    @PulseShape
+    public int getResponderTx() {
+        return mResponderTx;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (other instanceof CccPulseShapeCombo) {
+            CccPulseShapeCombo otherPulseShapeCombo = (CccPulseShapeCombo) other;
+            return otherPulseShapeCombo.mInitiatorTx == mInitiatorTx
+                    && otherPulseShapeCombo.mResponderTx == mResponderTx;
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(new int[] {mInitiatorTx, mResponderTx});
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/ccc/CccRangingError.java b/service/support_lib/src/com/google/uwb/support/ccc/CccRangingError.java
new file mode 100644
index 0000000..c7e57bf
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/ccc/CccRangingError.java
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.ccc;
+
+import android.os.Build.VERSION_CODES;
+import android.os.PersistableBundle;
+
+import androidx.annotation.RequiresApi;
+
+import com.google.uwb.support.base.RequiredParam;
+
+/**
+ * Defines parameters for CCC error reports
+ *
+ * <p>This passed as a bundle to the following callbacks, if the reason is {@link
+ * RangingSession.Callback.Reason#REASON_PROTOCOL_SPECIFIC_ERROR}:
+ *
+ * <ul>
+ *   <li>{@link RangingSession.Callback#onOpenFailed}
+ *   <li>{@link RangingSession.Callback#onStartFailed}
+ *   <li>{@link RangingSession.Callback#onReconfigureFailed}
+ *   <li>{@link RangingSession.Callback#onStopFailed}
+ *   <li>Any other {@code on*Failed} callback method.
+ * </ul>
+ */
+@RequiresApi(VERSION_CODES.LOLLIPOP)
+public class CccRangingError extends CccParams {
+    @ProtocolError private final int mError;
+
+    private static final String KEY_ERROR_CODE = "error_code";
+
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    private CccRangingError(@ProtocolError int error) {
+        mError = error;
+    }
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    public static CccRangingError fromBundle(PersistableBundle bundle) {
+        if (!isCorrectProtocol(bundle)) {
+            throw new IllegalArgumentException("Invalid protocol or protocol version");
+        }
+
+        switch (getBundleVersion(bundle)) {
+            case BUNDLE_VERSION_1:
+                return parseVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("Invalid bundle version");
+        }
+    }
+
+    private static CccRangingError parseVersion1(PersistableBundle bundle) {
+        return new Builder().setError(bundle.getInt(KEY_ERROR_CODE)).build();
+    }
+
+    @Override
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = super.toBundle();
+        bundle.putInt(KEY_ERROR_CODE, mError);
+        return bundle;
+    }
+
+    @ProtocolError
+    public int getError() {
+        return mError;
+    }
+
+    /** Builder */
+    public static final class Builder {
+        @ProtocolError private RequiredParam<Integer> mError = new RequiredParam<>();
+
+        public Builder setError(@ProtocolError int error) {
+            mError.set(error);
+            return this;
+        }
+
+        public CccRangingError build() {
+            return new CccRangingError(mError.get());
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/ccc/CccRangingReconfiguredParams.java b/service/support_lib/src/com/google/uwb/support/ccc/CccRangingReconfiguredParams.java
new file mode 100644
index 0000000..8469d26
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/ccc/CccRangingReconfiguredParams.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.ccc;
+
+import android.os.Build.VERSION_CODES;
+import android.os.PersistableBundle;
+import android.uwb.RangingSession;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * Defines parameters for CCC reconfigure operation
+ *
+ * <p>This is passed as a bundle to the client callback
+ * {@link RangingSession.Callback#onReconfigured}.
+ */
+@RequiresApi(VERSION_CODES.LOLLIPOP)
+public class CccRangingReconfiguredParams extends CccParams {
+
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    public static CccRangingReconfiguredParams fromBundle(PersistableBundle bundle) {
+        if (!isCorrectProtocol(bundle)) {
+            throw new IllegalArgumentException("Invalid protocol");
+        }
+
+        switch (getBundleVersion(bundle)) {
+            case BUNDLE_VERSION_1:
+                return parseVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("Invalid bundle version");
+        }
+    }
+
+    private static CccRangingReconfiguredParams parseVersion1(PersistableBundle unusedBundle) {
+        // Nothing to parse for now
+        return new CccRangingReconfiguredParams.Builder().build();
+    }
+
+    /** Builder */
+    public static class Builder {
+        public CccRangingReconfiguredParams build() {
+            return new CccRangingReconfiguredParams();
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/ccc/CccRangingStartedParams.java b/service/support_lib/src/com/google/uwb/support/ccc/CccRangingStartedParams.java
new file mode 100644
index 0000000..e70fb6e
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/ccc/CccRangingStartedParams.java
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.ccc;
+
+import android.os.Build.VERSION_CODES;
+import android.os.PersistableBundle;
+import android.uwb.RangingSession;
+
+import androidx.annotation.RequiresApi;
+
+import com.google.uwb.support.base.RequiredParam;
+
+/**
+ * Defines parameters for CCC start reports. The start operation can optionally include a request to
+ * reconfigure the RAN multiplier. On a reconfiguration, the CCC spec defines that the selected RAN
+ * multiplier shall be equal to or greater than the requested RAN multiplier, and therefore, on a
+ * reconfiguration, the selected RAN multiplier shall be populated in the CCC start report.
+ *
+ * <p>This is passed as a bundle to the client callback {@link RangingSession.Callback#onStarted}.
+ */
+@RequiresApi(VERSION_CODES.LOLLIPOP)
+public class CccRangingStartedParams extends CccParams {
+    private final int mStartingStsIndex;
+    private final long mUwbTime0;
+    private final int mHopModeKey;
+    @SyncCodeIndex private final int mSyncCodeIndex;
+    private final int mRanMultiplier;
+
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    private static final String KEY_STARTING_STS_INDEX = "starting_sts_index";
+    private static final String KEY_UWB_TIME_0 = "uwb_time_0";
+    private static final String KEY_HOP_MODE_KEY = "hop_mode_key";
+    private static final String KEY_SYNC_CODE_INDEX = "sync_code_index";
+    private static final String KEY_RAN_MULTIPLIER = "ran_multiplier";
+
+    private CccRangingStartedParams(Builder builder) {
+        mStartingStsIndex = builder.mStartingStsIndex.get();
+        mUwbTime0 = builder.mUwbTime0.get();
+        mHopModeKey = builder.mHopModeKey.get();
+        mSyncCodeIndex = builder.mSyncCodeIndex.get();
+        mRanMultiplier = builder.mRanMultiplier.get();
+    }
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    @Override
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = super.toBundle();
+        bundle.putInt(KEY_STARTING_STS_INDEX, mStartingStsIndex);
+        bundle.putLong(KEY_UWB_TIME_0, mUwbTime0);
+        bundle.putInt(KEY_HOP_MODE_KEY, mHopModeKey);
+        bundle.putInt(KEY_SYNC_CODE_INDEX, mSyncCodeIndex);
+        bundle.putInt(KEY_RAN_MULTIPLIER, mRanMultiplier);
+        return bundle;
+    }
+
+    public static CccRangingStartedParams fromBundle(PersistableBundle bundle) {
+        if (!isCorrectProtocol(bundle)) {
+            throw new IllegalArgumentException("Invalid protocol");
+        }
+
+        switch (getBundleVersion(bundle)) {
+            case BUNDLE_VERSION_1:
+                return parseVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("Invalid bundle version");
+        }
+    }
+
+    private static CccRangingStartedParams parseVersion1(PersistableBundle bundle) {
+        return new Builder()
+                .setStartingStsIndex(bundle.getInt(KEY_STARTING_STS_INDEX))
+                .setUwbTime0(bundle.getLong(KEY_UWB_TIME_0))
+                .setHopModeKey(bundle.getInt(KEY_HOP_MODE_KEY))
+                .setSyncCodeIndex(bundle.getInt(KEY_SYNC_CODE_INDEX))
+                .setRanMultiplier(bundle.getInt(KEY_RAN_MULTIPLIER))
+                .build();
+    }
+
+    public int getStartingStsIndex() {
+        return mStartingStsIndex;
+    }
+
+    public long getUwbTime0() {
+        return mUwbTime0;
+    }
+
+    public int getHopModeKey() {
+        return mHopModeKey;
+    }
+
+    @SyncCodeIndex
+    public int getSyncCodeIndex() {
+        return mSyncCodeIndex;
+    }
+
+    public int getRanMultiplier() {
+        return mRanMultiplier;
+    }
+
+    /** Builder */
+    public static final class Builder {
+        private RequiredParam<Integer> mStartingStsIndex = new RequiredParam<>();
+        private RequiredParam<Long> mUwbTime0 = new RequiredParam<>();
+        private RequiredParam<Integer> mHopModeKey = new RequiredParam<>();
+        @SyncCodeIndex private RequiredParam<Integer> mSyncCodeIndex = new RequiredParam<>();
+        private RequiredParam<Integer> mRanMultiplier = new RequiredParam<>();
+
+        public Builder setStartingStsIndex(int startingStsIndex) {
+            mStartingStsIndex.set(startingStsIndex);
+            return this;
+        }
+
+        public Builder setUwbTime0(long uwbTime0) {
+            mUwbTime0.set(uwbTime0);
+            return this;
+        }
+
+        public Builder setHopModeKey(int hopModeKey) {
+            mHopModeKey.set(hopModeKey);
+            return this;
+        }
+
+        public Builder setSyncCodeIndex(@SyncCodeIndex int syncCodeIndex) {
+            mSyncCodeIndex.set(syncCodeIndex);
+            return this;
+        }
+
+        public Builder setRanMultiplier(int ranMultiplier) {
+            mRanMultiplier.set(ranMultiplier);
+            return this;
+        }
+
+        public CccRangingStartedParams build() {
+            return new CccRangingStartedParams(this);
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/ccc/CccSpecificationParams.java b/service/support_lib/src/com/google/uwb/support/ccc/CccSpecificationParams.java
new file mode 100644
index 0000000..105aeab
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/ccc/CccSpecificationParams.java
@@ -0,0 +1,360 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.ccc;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.os.Build.VERSION_CODES;
+import android.os.PersistableBundle;
+import android.uwb.UwbManager;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import com.google.uwb.support.base.RequiredParam;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Defines parameters for CCC capability reports
+ *
+ * <p>This is returned as a bundle from the service API {@link UwbManager#getSpecificationInfo}.
+ */
+@RequiresApi(VERSION_CODES.LOLLIPOP)
+public class CccSpecificationParams extends CccParams {
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    private final List<CccProtocolVersion> mProtocolVersions;
+    @UwbConfig private final List<Integer> mUwbConfigs;
+    private final List<CccPulseShapeCombo> mPulseShapeCombos;
+    private final int mRanMultiplier;
+    @ChapsPerSlot private final List<Integer> mChapsPerSlot;
+    @SyncCodeIndex private final List<Integer> mSyncCodes;
+    @Channel private final List<Integer> mChannels;
+    @HoppingConfigMode private final List<Integer> mHoppingConfigModes;
+    @HoppingSequence private final List<Integer> mHoppingSequences;
+
+    private static final String KEY_PROTOCOL_VERSIONS = "protocol_versions";
+    private static final String KEY_UWB_CONFIGS = "uwb_configs";
+    private static final String KEY_PULSE_SHAPE_COMBOS = "pulse_shape_combos";
+    private static final String KEY_RAN_MULTIPLIER = "ran_multiplier";
+    private static final String KEY_CHAPS_PER_SLOTS = "chaps_per_slots";
+    private static final String KEY_SYNC_CODES = "sync_codes";
+    private static final String KEY_CHANNELS = "channels";
+    private static final String KEY_HOPPING_CONFIGS = "hopping_config_modes";
+    private static final String KEY_HOPPING_SEQUENCES = "hopping_sequences";
+
+    private CccSpecificationParams(
+            List<CccProtocolVersion> protocolVersions,
+            @UwbConfig List<Integer> uwbConfigs,
+            List<CccPulseShapeCombo> pulseShapeCombos,
+            int ranMultiplier,
+            @ChapsPerSlot List<Integer> chapsPerSlot,
+            @SyncCodeIndex List<Integer> syncCodes,
+            @Channel List<Integer> channels,
+            @HoppingConfigMode List<Integer> hoppingConfigModes,
+            @HoppingSequence List<Integer> hoppingSequences) {
+        mProtocolVersions = protocolVersions;
+        mUwbConfigs = uwbConfigs;
+        mPulseShapeCombos = pulseShapeCombos;
+        mRanMultiplier = ranMultiplier;
+        mChapsPerSlot = chapsPerSlot;
+        mSyncCodes = syncCodes;
+        mChannels = channels;
+        mHoppingConfigModes = hoppingConfigModes;
+        mHoppingSequences = hoppingSequences;
+    }
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    @Override
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = super.toBundle();
+        String[] protocols = new String[mProtocolVersions.size()];
+        for (int i = 0; i < protocols.length; i++) {
+            protocols[i] = mProtocolVersions.get(i).toString();
+        }
+        String[] pulseShapeCombos = new String[mPulseShapeCombos.size()];
+        for (int i = 0; i < pulseShapeCombos.length; i++) {
+            pulseShapeCombos[i] = mPulseShapeCombos.get(i).toString();
+        }
+        bundle.putStringArray(KEY_PROTOCOL_VERSIONS, protocols);
+        bundle.putIntArray(KEY_UWB_CONFIGS, toIntArray(mUwbConfigs));
+        bundle.putStringArray(KEY_PULSE_SHAPE_COMBOS, pulseShapeCombos);
+        bundle.putInt(KEY_RAN_MULTIPLIER, mRanMultiplier);
+        bundle.putIntArray(KEY_CHAPS_PER_SLOTS, toIntArray(mChapsPerSlot));
+        bundle.putIntArray(KEY_SYNC_CODES, toIntArray(mSyncCodes));
+        bundle.putIntArray(KEY_CHANNELS, toIntArray(mChannels));
+        bundle.putIntArray(KEY_HOPPING_CONFIGS, toIntArray(mHoppingConfigModes));
+        bundle.putIntArray(KEY_HOPPING_SEQUENCES, toIntArray(mHoppingSequences));
+        return bundle;
+    }
+
+    public static CccSpecificationParams fromBundle(PersistableBundle bundle) {
+        if (!isCorrectProtocol(bundle)) {
+            throw new IllegalArgumentException("Invalid protocol");
+        }
+
+        switch (getBundleVersion(bundle)) {
+            case BUNDLE_VERSION_1:
+                return parseVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("Invalid bundle version");
+        }
+    }
+
+    private static CccSpecificationParams parseVersion1(PersistableBundle bundle) {
+        CccSpecificationParams.Builder builder = new CccSpecificationParams.Builder();
+        String[] protocolStrings = checkNotNull(bundle.getStringArray(KEY_PROTOCOL_VERSIONS));
+        for (String protocol : protocolStrings) {
+            builder.addProtocolVersion(CccProtocolVersion.fromString(protocol));
+        }
+
+        for (int config : checkNotNull(bundle.getIntArray(KEY_UWB_CONFIGS))) {
+            builder.addUwbConfig(config);
+        }
+
+        String[] pulseShapeComboStrings =
+                checkNotNull(bundle.getStringArray(KEY_PULSE_SHAPE_COMBOS));
+        for (String pulseShapeCombo : pulseShapeComboStrings) {
+            builder.addPulseShapeCombo(CccPulseShapeCombo.fromString(pulseShapeCombo));
+        }
+
+        builder.setRanMultiplier(bundle.getInt(KEY_RAN_MULTIPLIER));
+
+        for (int chapsPerSlot : checkNotNull(bundle.getIntArray(KEY_CHAPS_PER_SLOTS))) {
+            builder.addChapsPerSlot(chapsPerSlot);
+        }
+
+        for (int syncCode : checkNotNull(bundle.getIntArray(KEY_SYNC_CODES))) {
+            builder.addSyncCode(syncCode);
+        }
+
+        for (int channel : checkNotNull(bundle.getIntArray(KEY_CHANNELS))) {
+            builder.addChannel(channel);
+        }
+
+        for (int hoppingConfig : checkNotNull(bundle.getIntArray(KEY_HOPPING_CONFIGS))) {
+            builder.addHoppingConfigMode(hoppingConfig);
+        }
+
+        for (int hoppingSequence : checkNotNull(bundle.getIntArray(KEY_HOPPING_SEQUENCES))) {
+            builder.addHoppingSequence(hoppingSequence);
+        }
+
+        return builder.build();
+    }
+
+    private int[] toIntArray(List<Integer> data) {
+        int[] res = new int[data.size()];
+        for (int i = 0; i < data.size(); i++) {
+            res[i] = data.get(i);
+        }
+        return res;
+    }
+
+    public List<CccProtocolVersion> getProtocolVersions() {
+        return mProtocolVersions;
+    }
+
+    @UwbConfig
+    public List<Integer> getUwbConfigs() {
+        return mUwbConfigs;
+    }
+
+    public List<CccPulseShapeCombo> getPulseShapeCombos() {
+        return mPulseShapeCombos;
+    }
+
+    @IntRange(from = 0, to = 255)
+    public int getRanMultiplier() {
+        return mRanMultiplier;
+    }
+
+    @ChapsPerSlot
+    public List<Integer> getChapsPerSlot() {
+        return mChapsPerSlot;
+    }
+
+    @SyncCodeIndex
+    public List<Integer> getSyncCodes() {
+        return mSyncCodes;
+    }
+
+    @Channel
+    public List<Integer> getChannels() {
+        return mChannels;
+    }
+
+    @HoppingSequence
+    public List<Integer> getHoppingSequences() {
+        return mHoppingSequences;
+    }
+
+    @HoppingConfigMode
+    public List<Integer> getHoppingConfigModes() {
+        return mHoppingConfigModes;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (other instanceof CccSpecificationParams) {
+            CccSpecificationParams otherSpecificationParams = (CccSpecificationParams) other;
+            return otherSpecificationParams.mProtocolVersions.equals(mProtocolVersions)
+                && otherSpecificationParams.mPulseShapeCombos.equals(mPulseShapeCombos)
+                && otherSpecificationParams.mUwbConfigs.equals(mUwbConfigs)
+                && otherSpecificationParams.mRanMultiplier == mRanMultiplier
+                && otherSpecificationParams.mChapsPerSlot.equals(mChapsPerSlot)
+                && otherSpecificationParams.mSyncCodes.equals(mSyncCodes)
+                && otherSpecificationParams.mChannels.equals(mChannels)
+                && otherSpecificationParams.mHoppingConfigModes.equals(mHoppingConfigModes)
+                && otherSpecificationParams.mHoppingSequences.equals(mHoppingSequences);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(
+            new int[] {
+                mProtocolVersions.hashCode(),
+                mPulseShapeCombos.hashCode(),
+                mUwbConfigs.hashCode(),
+                mRanMultiplier,
+                mChapsPerSlot.hashCode(),
+                mSyncCodes.hashCode(),
+                mChannels.hashCode(),
+                mHoppingConfigModes.hashCode(),
+                mHoppingSequences.hashCode()
+            });
+    }
+
+    /** Builder */
+    public static class Builder {
+        private List<CccProtocolVersion> mProtocolVersions = new ArrayList<>();
+        @UwbConfig private List<Integer> mUwbConfigs = new ArrayList<>();
+        private List<CccPulseShapeCombo> mPulseShapeCombos = new ArrayList<>();
+        private RequiredParam<Integer> mRanMultiplier = new RequiredParam<>();
+        @ChapsPerSlot private List<Integer> mChapsPerSlot = new ArrayList<>();
+        @SyncCodeIndex private List<Integer> mSyncCodes = new ArrayList<>();
+        @Channel private List<Integer> mChannels = new ArrayList<>();
+        @HoppingSequence private List<Integer> mHoppingSequences = new ArrayList<>();
+        @HoppingConfigMode private List<Integer> mHoppingConfigModes = new ArrayList<>();
+
+        public Builder addProtocolVersion(@NonNull CccProtocolVersion version) {
+            mProtocolVersions.add(version);
+            return this;
+        }
+
+        public Builder addUwbConfig(@UwbConfig int uwbConfig) {
+            mUwbConfigs.add(uwbConfig);
+            return this;
+        }
+
+        public Builder addPulseShapeCombo(CccPulseShapeCombo pulseShapeCombo) {
+            mPulseShapeCombos.add(pulseShapeCombo);
+            return this;
+        }
+
+        public Builder setRanMultiplier(int ranMultiplier) {
+            if (ranMultiplier < 0 || ranMultiplier > 255) {
+                throw new IllegalArgumentException("Invalid RAN Multiplier");
+            }
+            mRanMultiplier.set(ranMultiplier);
+            return this;
+        }
+
+        public Builder addChapsPerSlot(@ChapsPerSlot int chapsPerSlot) {
+            mChapsPerSlot.add(chapsPerSlot);
+            return this;
+        }
+
+        public Builder addSyncCode(@SyncCodeIndex int syncCode) {
+            mSyncCodes.add(syncCode);
+            return this;
+        }
+
+        public Builder addChannel(@Channel int channel) {
+            mChannels.add(channel);
+            return this;
+        }
+
+        public Builder addHoppingConfigMode(@HoppingConfigMode int hoppingConfigMode) {
+            mHoppingConfigModes.add(hoppingConfigMode);
+            return this;
+        }
+
+        public Builder addHoppingSequence(@HoppingSequence int hoppingSequence) {
+            mHoppingSequences.add(hoppingSequence);
+            return this;
+        }
+
+        public CccSpecificationParams build() {
+            if (mProtocolVersions.size() == 0) {
+                throw new IllegalStateException("No protocol versions set");
+            }
+
+            if (mUwbConfigs.size() == 0) {
+                throw new IllegalStateException("No UWB Configs set");
+            }
+
+            if (mPulseShapeCombos.size() == 0) {
+                throw new IllegalStateException("No Pulse Shape Combos set");
+            }
+
+            if (mChapsPerSlot.size() == 0) {
+                throw new IllegalStateException("No Slot Durations set");
+            }
+
+            if (mSyncCodes.size() == 0) {
+                throw new IllegalStateException("No Sync Codes set");
+            }
+
+            if (mChannels.size() == 0) {
+                throw new IllegalStateException("No channels set");
+            }
+
+            if (mHoppingConfigModes.size() == 0) {
+                throw new IllegalStateException("No hopping config modes set");
+            }
+
+            if (mHoppingSequences.size() == 0) {
+                throw new IllegalStateException("No hopping sequences set");
+            }
+
+            return new CccSpecificationParams(
+                    mProtocolVersions,
+                    mUwbConfigs,
+                    mPulseShapeCombos,
+                    mRanMultiplier.get(),
+                    mChapsPerSlot,
+                    mSyncCodes,
+                    mChannels,
+                    mHoppingConfigModes,
+                    mHoppingSequences);
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/ccc/CccStartRangingParams.java b/service/support_lib/src/com/google/uwb/support/ccc/CccStartRangingParams.java
new file mode 100644
index 0000000..d543f86
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/ccc/CccStartRangingParams.java
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.ccc;
+
+import android.os.Build.VERSION_CODES;
+import android.os.PersistableBundle;
+import android.uwb.RangingSession;
+
+import androidx.annotation.RequiresApi;
+
+import com.google.uwb.support.base.RequiredParam;
+
+/**
+ * Defines optional parameters for CCC start operation. These parameters are only required if a
+ * reconfiguration of the RAN multiplier is required. These parameters are used to support the
+ * Configurable_Ranging_Recovery_RQ message in the CCC specification. Start, or start with RAN
+ * multiplier reconfiguration can only be called on a stopped session.
+ *
+ * <p>This is passed as a bundle to the service API {@link RangingSession#start}.
+ */
+@RequiresApi(VERSION_CODES.LOLLIPOP)
+public class CccStartRangingParams extends CccParams {
+
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    private static final String KEY_SESSION_ID = "session_id";
+    private static final String KEY_RAN_MULTIPLIER = "ran_multiplier";
+
+    private final int mSessionId;
+    private final int mRanMultiplier;
+
+    private CccStartRangingParams(Builder builder) {
+        this.mSessionId = builder.mSessionId.get();
+        this.mRanMultiplier = builder.mRanMultiplier.get();
+    }
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    @Override
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = super.toBundle();
+        bundle.putInt(KEY_SESSION_ID, mSessionId);
+        bundle.putInt(KEY_RAN_MULTIPLIER, mRanMultiplier);
+        return bundle;
+    }
+
+    public static CccStartRangingParams fromBundle(PersistableBundle bundle) {
+        if (!isCorrectProtocol(bundle)) {
+            throw new IllegalArgumentException("Invalid protocol");
+        }
+
+        switch (getBundleVersion(bundle)) {
+            case BUNDLE_VERSION_1:
+                return parseVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("Invalid bundle version");
+        }
+    }
+
+    public int getSessionId() {
+        return mSessionId;
+    }
+
+    public int getRanMultiplier() {
+        return mRanMultiplier;
+    }
+
+    private static CccStartRangingParams parseVersion1(PersistableBundle bundle) {
+        return new Builder()
+            .setSessionId(bundle.getInt(KEY_SESSION_ID))
+            .setRanMultiplier(bundle.getInt(KEY_RAN_MULTIPLIER))
+            .build();
+    }
+
+    /** Builder */
+    public static class Builder {
+        private RequiredParam<Integer> mSessionId = new RequiredParam<>();
+        private RequiredParam<Integer> mRanMultiplier = new RequiredParam<>();
+
+        public Builder setSessionId(int sessionId) {
+            mSessionId.set(sessionId);
+            return this;
+        }
+
+        public Builder setRanMultiplier(int ranMultiplier) {
+            mRanMultiplier.set(ranMultiplier);
+            return this;
+        }
+
+        public CccStartRangingParams build() {
+            return new CccStartRangingParams(this);
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/fira/FiraControleeParams.java b/service/support_lib/src/com/google/uwb/support/fira/FiraControleeParams.java
new file mode 100644
index 0000000..89df692
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/fira/FiraControleeParams.java
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.fira;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import static java.util.Objects.requireNonNull;
+
+import android.os.PersistableBundle;
+import android.uwb.RangingSession;
+import android.uwb.UwbAddress;
+import android.uwb.UwbManager;
+
+import androidx.annotation.Nullable;
+
+/**
+ * UWB parameters used to add/remove controlees for a FiRa session
+ *
+ * <p>This is passed as a bundle to the service API {@link RangingSession#addControlee} and
+ * {@link RangingSession#removeControlee}.
+ */
+public class FiraControleeParams extends FiraParams {
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    @Nullable private final UwbAddress[] mAddressList;
+    @Nullable private final int[] mSubSessionIdList;
+
+    private static final String KEY_MAC_ADDRESS_MODE = "mac_address_mode";
+    private static final String KEY_ADDRESS_LIST = "address_list";
+    private static final String KEY_SUB_SESSION_ID_LIST = "sub_session_id_list";
+
+    private FiraControleeParams(
+            @Nullable UwbAddress[] addressList,
+            @Nullable int[] subSessionIdList) {
+        mAddressList = addressList;
+        mSubSessionIdList = subSessionIdList;
+    }
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    @Nullable
+    public UwbAddress[] getAddressList() {
+        return mAddressList;
+    }
+
+    @Nullable
+    public int[] getSubSessionIdList() {
+        return mSubSessionIdList;
+    }
+    @Override
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = super.toBundle();
+        requireNonNull(mAddressList);
+
+        long[] addressList = new long[mAddressList.length];
+        int i = 0;
+        for (UwbAddress address : mAddressList) {
+            addressList[i++] = uwbAddressToLong(address);
+        }
+        int macAddressMode = MAC_ADDRESS_MODE_2_BYTES;
+        if (mAddressList[0].size() == UwbAddress.EXTENDED_ADDRESS_BYTE_LENGTH) {
+            macAddressMode = MAC_ADDRESS_MODE_8_BYTES;
+        }
+        bundle.putInt(KEY_MAC_ADDRESS_MODE, macAddressMode);
+        bundle.putLongArray(KEY_ADDRESS_LIST, addressList);
+        bundle.putIntArray(KEY_SUB_SESSION_ID_LIST, mSubSessionIdList);
+        return bundle;
+    }
+
+    public static FiraControleeParams fromBundle(PersistableBundle bundle) {
+        if (!isCorrectProtocol(bundle)) {
+            throw new IllegalArgumentException("Invalid protocol");
+        }
+
+        switch (getBundleVersion(bundle)) {
+            case BUNDLE_VERSION_1:
+                return parseVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("Invalid bundle version");
+        }
+    }
+
+    private static FiraControleeParams parseVersion1(PersistableBundle bundle) {
+        FiraControleeParams.Builder builder = new FiraControleeParams.Builder();
+        int macAddressMode = bundle.getInt(KEY_MAC_ADDRESS_MODE);
+        int addressByteLength = UwbAddress.SHORT_ADDRESS_BYTE_LENGTH;
+        if (macAddressMode == MAC_ADDRESS_MODE_8_BYTES) {
+            addressByteLength = UwbAddress.EXTENDED_ADDRESS_BYTE_LENGTH;
+        }
+        long[] addresses = bundle.getLongArray(KEY_ADDRESS_LIST);
+        UwbAddress[] addressList = new UwbAddress[addresses.length];
+        for (int i = 0; i < addresses.length; i++) {
+            addressList[i] = longToUwbAddress(addresses[i], addressByteLength);
+        }
+        builder.setAddressList(addressList);
+        builder.setSubSessionIdList(bundle.getIntArray(KEY_SUB_SESSION_ID_LIST));
+        return builder.build();
+    }
+
+    /** Builder */
+    public static class Builder {
+        @Nullable private UwbAddress[] mAddressList = null;
+        @Nullable private int[] mSubSessionIdList = null;
+
+        public FiraControleeParams.Builder setAddressList(UwbAddress[] addressList) {
+            mAddressList = addressList;
+            return this;
+        }
+
+        public FiraControleeParams.Builder setSubSessionIdList(int[] subSessionIdList) {
+            mSubSessionIdList = subSessionIdList;
+            return this;
+        }
+
+        private void checkAddressList() {
+            checkArgument(mAddressList != null && mAddressList.length > 0);
+            for (UwbAddress uwbAddress : mAddressList) {
+                requireNonNull(uwbAddress);
+                checkArgument(uwbAddress.size() == UwbAddress.SHORT_ADDRESS_BYTE_LENGTH);
+            }
+
+            checkArgument(
+                    mSubSessionIdList == null || mSubSessionIdList.length == mAddressList.length);
+        }
+
+        public FiraControleeParams build() {
+            checkAddressList();
+            return new FiraControleeParams(
+                    mAddressList,
+                    mSubSessionIdList);
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/fira/FiraMulticastListUpdateStatusCode.java b/service/support_lib/src/com/google/uwb/support/fira/FiraMulticastListUpdateStatusCode.java
new file mode 100644
index 0000000..f1249f1
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/fira/FiraMulticastListUpdateStatusCode.java
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.fira;
+
+import android.os.PersistableBundle;
+
+import com.google.uwb.support.base.RequiredParam;
+
+/** FiRa Multicast List update status code defined in UCI 1.0 Table 27 */
+public class FiraMulticastListUpdateStatusCode extends FiraParams {
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    @MulticastListUpdateStatus private final int mStatusCode;
+
+    private static final String KEY_STATUS_CODE = "multicast_list_update_status_code";
+
+    private FiraMulticastListUpdateStatusCode(@MulticastListUpdateStatus int statusCode) {
+        mStatusCode = statusCode;
+    }
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    @MulticastListUpdateStatus
+    public int getStatusCode() {
+        return mStatusCode;
+    }
+
+    @Override
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = super.toBundle();
+        bundle.putInt(KEY_STATUS_CODE, mStatusCode);
+        return bundle;
+    }
+
+    public static FiraMulticastListUpdateStatusCode fromBundle(PersistableBundle bundle) {
+        if (!isCorrectProtocol(bundle)) {
+            throw new IllegalArgumentException("Invalid protocol");
+        }
+
+        switch (getBundleVersion(bundle)) {
+            case BUNDLE_VERSION_1:
+                return parseVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("Invalid bundle version");
+        }
+    }
+
+    public static boolean isBundleValid(PersistableBundle bundle) {
+        return bundle.containsKey(KEY_STATUS_CODE);
+    }
+
+    private static FiraMulticastListUpdateStatusCode parseVersion1(PersistableBundle bundle) {
+        return new FiraMulticastListUpdateStatusCode.Builder()
+                .setStatusCode(bundle.getInt(KEY_STATUS_CODE))
+                .build();
+    }
+
+    /** Builder */
+    public static class Builder {
+        private final RequiredParam<Integer> mStatusCode = new RequiredParam<>();
+
+        public FiraMulticastListUpdateStatusCode.Builder setStatusCode(int statusCode) {
+            mStatusCode.set(statusCode);
+            return this;
+        }
+
+        public FiraMulticastListUpdateStatusCode build() {
+            return new FiraMulticastListUpdateStatusCode(mStatusCode.get());
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/fira/FiraOpenSessionParams.java b/service/support_lib/src/com/google/uwb/support/fira/FiraOpenSessionParams.java
new file mode 100644
index 0000000..3ea8b91
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/fira/FiraOpenSessionParams.java
@@ -0,0 +1,1267 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.fira;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import static com.google.uwb.support.fira.FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_INTERLEAVED;
+
+import static java.util.Objects.requireNonNull;
+
+import android.os.PersistableBundle;
+import android.uwb.UwbAddress;
+import android.uwb.UwbManager;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.uwb.support.base.RequiredParam;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * UWB parameters used to open a FiRa session.
+ *
+ * <p>This is passed as a bundle to the service API {@link UwbManager#openRangingSession}.
+ */
+public class FiraOpenSessionParams extends FiraParams {
+    private final FiraProtocolVersion mProtocolVersion;
+
+    private final int mSessionId;
+    @RangingDeviceType private final int mDeviceType;
+    @RangingDeviceRole private final int mDeviceRole;
+    @RangingRoundUsage private final int mRangingRoundUsage;
+    @MultiNodeMode private final int mMultiNodeMode;
+
+    private final UwbAddress mDeviceAddress;
+
+    // Dest address list
+    private final List<UwbAddress> mDestAddressList;
+
+    private final int mInitiationTimeMs;
+    private final int mSlotDurationRstu;
+    private final int mSlotsPerRangingRound;
+    private final int mRangingIntervalMs;
+    private final int mBlockStrideLength;
+    private final int mHoppingMode;
+
+    @IntRange(from = 0, to = 65535)
+    private final int mMaxRangingRoundRetries;
+
+    private final int mSessionPriority;
+    @MacAddressMode final int mMacAddressMode;
+    private final boolean mHasResultReportPhase;
+    @MeasurementReportType private final int mMeasurementReportType;
+
+    @IntRange(from = 1, to = 10)
+    private final int mInBandTerminationAttemptCount;
+
+    @UwbChannel private final int mChannelNumber;
+    private final int mPreambleCodeIndex;
+    @RframeConfig private final int mRframeConfig;
+    @PrfMode private final int mPrfMode;
+    @PreambleDuration private final int mPreambleDuration;
+    @SfdIdValue private final int mSfdId;
+    @StsSegmentCountValue private final int mStsSegmentCount;
+    @StsLength private final int mStsLength;
+    @PsduDataRate private final int mPsduDataRate;
+    @BprfPhrDataRate private final int mBprfPhrDataRate;
+    @MacFcsType private final int mFcsType;
+    private final boolean mIsTxAdaptivePayloadPowerEnabled;
+    @StsConfig private final int mStsConfig;
+    private final int mSubSessionId;
+    @AoaType private final int mAoaType;
+
+    // 2-byte long array
+    @Nullable private final byte[] mVendorId;
+
+    // 6-byte long array
+    @Nullable private final byte[] mStaticStsIV;
+
+    private final boolean mIsKeyRotationEnabled;
+    private final int mKeyRotationRate;
+    @AoaResultRequestMode private final int mAoaResultRequest;
+    @RangeDataNtfConfig private final int mRangeDataNtfConfig;
+    private final int mRangeDataNtfProximityNear;
+    private final int mRangeDataNtfProximityFar;
+    private final boolean mHasTimeOfFlightReport;
+    private final boolean mHasAngleOfArrivalAzimuthReport;
+    private final boolean mHasAngleOfArrivalElevationReport;
+    private final boolean mHasAngleOfArrivalFigureOfMeritReport;
+    private final int mNumOfMsrmtFocusOnRange;
+    private final int mNumOfMsrmtFocusOnAoaAzimuth;
+    private final int mNumOfMsrmtFocusOnAoaElevation;
+
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    private static final String KEY_PROTOCOL_VERSION = "protocol_version";
+    private static final String KEY_SESSION_ID = "session_id";
+    private static final String KEY_DEVICE_TYPE = "device_type";
+    private static final String KEY_DEVICE_ROLE = "device_role";
+    private static final String KEY_RANGING_ROUND_USAGE = "ranging_round_usage";
+    private static final String KEY_MULTI_NODE_MODE = "multi_node_mode";
+    private static final String KEY_DEVICE_ADDRESS = "device_address";
+    private static final String KEY_DEST_ADDRESS_LIST = "dest_address_list";
+    private static final String KEY_INITIATION_TIME_MS = "initiation_time_ms";
+    private static final String KEY_SLOT_DURATION_RSTU = "slot_duration_rstu";
+    private static final String KEY_SLOTS_PER_RANGING_ROUND = "slots_per_ranging_round";
+    private static final String KEY_RANGING_INTERVAL_MS = "ranging_interval_ms";
+    private static final String KEY_BLOCK_STRIDE_LENGTH = "block_stride_length";
+    private static final String KEY_HOPPING_MODE = "hopping_mode";
+    private static final String KEY_MAX_RANGING_ROUND_RETRIES = "max_ranging_round_retries";
+    private static final String KEY_SESSION_PRIORITY = "session_priority";
+    private static final String KEY_MAC_ADDRESS_MODE = "mac_address_mode";
+    private static final String KEY_IN_BAND_TERMINATION_ATTEMPT_COUNT =
+            "in_band_termination_attempt_count";
+    private static final String KEY_CHANNEL_NUMBER = "channel_number";
+    private static final String KEY_PREAMBLE_CODE_INDEX = "preamble_code_index";
+    private static final String KEY_RFRAME_CONFIG = "rframe_config";
+    private static final String KEY_PRF_MODE = "prf_mode";
+    private static final String KEY_PREAMBLE_DURATION = "preamble_duration";
+    private static final String KEY_SFD_ID = "sfd_id";
+    private static final String KEY_STS_SEGMENT_COUNT = "sts_segment_count";
+    private static final String KEY_STS_LENGTH = "sts_length";
+    private static final String KEY_PSDU_DATA_RATE = "psdu_data_rate";
+    private static final String KEY_BPRF_PHR_DATA_RATE = "bprf_phr_data_rate";
+    private static final String KEY_FCS_TYPE = "fcs_type";
+    private static final String KEY_IS_TX_ADAPTIVE_PAYLOAD_POWER_ENABLED =
+            "is_tx_adaptive_payload_power_enabled";
+    private static final String KEY_STS_CONFIG = "sts_config";
+    private static final String KEY_SUB_SESSION_ID = "sub_session_id";
+    private static final String KEY_VENDOR_ID = "vendor_id";
+    private static final String KEY_STATIC_STS_IV = "static_sts_iv";
+    private static final String KEY_IS_KEY_ROTATION_ENABLED = "is_key_rotation_enabled";
+    private static final String KEY_KEY_ROTATION_RATE = "key_rotation_rate";
+    private static final String KEY_AOA_RESULT_REQUEST = "aoa_result_request";
+    private static final String KEY_RANGE_DATA_NTF_CONFIG = "range_data_ntf_config";
+    private static final String KEY_RANGE_DATA_NTF_PROXIMITY_NEAR = "range_data_ntf_proximity_near";
+    private static final String KEY_RANGE_DATA_NTF_PROXIMITY_FAR = "range_data_ntf_proximity_far";
+    private static final String KEY_HAS_TIME_OF_FLIGHT_REPORT = "has_time_of_flight_report";
+    private static final String KEY_HAS_ANGLE_OF_ARRIVAL_AZIMUTH_REPORT =
+            "has_angle_of_arrival_azimuth_report";
+    private static final String KEY_HAS_ANGLE_OF_ARRIVAL_ELEVATION_REPORT =
+            "has_angle_of_arrival_elevation_report";
+    private static final String KEY_HAS_ANGLE_OF_ARRIVAL_FIGURE_OF_MERIT_REPORT =
+            "has_angle_of_arrival_figure_of_merit_report";
+    private static final String KEY_HAS_RESULT_REPORT_PHASE = "has_result_report_phase";
+    private static final String KEY_MEASUREMENT_REPORT_TYPE = "measurement_report_type";
+    private static final String KEY_AOA_TYPE = "aoa_type";
+    private static final String KEY_NUM_OF_MSRMT_FOCUS_ON_RANGE =
+            "num_of_msrmt_focus_on_range";
+    private static final String KEY_NUM_OF_MSRMT_FOCUS_ON_AOA_AZIMUTH =
+            "num_of_msrmt_focus_on_aoa_azimuth";
+    private static final String KEY_NUM_OF_MSRMT_FOCUS_ON_AOA_ELEVATION =
+            "num_of_msrmt_focus_on_aoa_elevation";
+
+    private FiraOpenSessionParams(
+            FiraProtocolVersion protocolVersion,
+            int sessionId,
+            @RangingDeviceType int deviceType,
+            @RangingDeviceRole int deviceRole,
+            @RangingRoundUsage int rangingRoundUsage,
+            @MultiNodeMode int multiNodeMode,
+            UwbAddress deviceAddress,
+            List<UwbAddress> destAddressList,
+            int initiationTimeMs,
+            int slotDurationRstu,
+            int slotsPerRangingRound,
+            int rangingIntervalMs,
+            int blockStrideLength,
+            int hoppingMode,
+            @IntRange(from = 0, to = 65535) int maxRangingRoundRetries,
+            int sessionPriority,
+            @MacAddressMode int macAddressMode,
+            boolean hasResultReportPhase,
+            @MeasurementReportType int measurementReportType,
+            @IntRange(from = 1, to = 10) int inBandTerminationAttemptCount,
+            @UwbChannel int channelNumber,
+            int preambleCodeIndex,
+            @RframeConfig int rframeConfig,
+            @PrfMode int prfMode,
+            @PreambleDuration int preambleDuration,
+            @SfdIdValue int sfdId,
+            @StsSegmentCountValue int stsSegmentCount,
+            @StsLength int stsLength,
+            @PsduDataRate int psduDataRate,
+            @BprfPhrDataRate int bprfPhrDataRate,
+            @MacFcsType int fcsType,
+            boolean isTxAdaptivePayloadPowerEnabled,
+            @StsConfig int stsConfig,
+            int subSessionId,
+            @Nullable byte[] vendorId,
+            @Nullable byte[] staticStsIV,
+            boolean isKeyRotationEnabled,
+            int keyRotationRate,
+            @AoaResultRequestMode int aoaResultRequest,
+            @RangeDataNtfConfig int rangeDataNtfConfig,
+            int rangeDataNtfProximityNear,
+            int rangeDataNtfProximityFar,
+            boolean hasTimeOfFlightReport,
+            boolean hasAngleOfArrivalAzimuthReport,
+            boolean hasAngleOfArrivalElevationReport,
+            boolean hasAngleOfArrivalFigureOfMeritReport,
+            @AoaType int aoaType,
+            int numOfMsrmtFocusOnRange,
+            int numOfMsrmtFocusOnAoaAzimuth,
+            int numOfMsrmtFocusOnAoaElevation) {
+        mProtocolVersion = protocolVersion;
+        mSessionId = sessionId;
+        mDeviceType = deviceType;
+        mDeviceRole = deviceRole;
+        mRangingRoundUsage = rangingRoundUsage;
+        mMultiNodeMode = multiNodeMode;
+        mDeviceAddress = deviceAddress;
+        mDestAddressList = destAddressList;
+        mInitiationTimeMs = initiationTimeMs;
+        mSlotDurationRstu = slotDurationRstu;
+        mSlotsPerRangingRound = slotsPerRangingRound;
+        mRangingIntervalMs = rangingIntervalMs;
+        mBlockStrideLength = blockStrideLength;
+        mHoppingMode = hoppingMode;
+        mMaxRangingRoundRetries = maxRangingRoundRetries;
+        mSessionPriority = sessionPriority;
+        mMacAddressMode = macAddressMode;
+        mHasResultReportPhase = hasResultReportPhase;
+        mMeasurementReportType = measurementReportType;
+        mInBandTerminationAttemptCount = inBandTerminationAttemptCount;
+        mChannelNumber = channelNumber;
+        mPreambleCodeIndex = preambleCodeIndex;
+        mRframeConfig = rframeConfig;
+        mPrfMode = prfMode;
+        mPreambleDuration = preambleDuration;
+        mSfdId = sfdId;
+        mStsSegmentCount = stsSegmentCount;
+        mStsLength = stsLength;
+        mPsduDataRate = psduDataRate;
+        mBprfPhrDataRate = bprfPhrDataRate;
+        mFcsType = fcsType;
+        mIsTxAdaptivePayloadPowerEnabled = isTxAdaptivePayloadPowerEnabled;
+        mStsConfig = stsConfig;
+        mSubSessionId = subSessionId;
+        mVendorId = vendorId;
+        mStaticStsIV = staticStsIV;
+        mIsKeyRotationEnabled = isKeyRotationEnabled;
+        mKeyRotationRate = keyRotationRate;
+        mAoaResultRequest = aoaResultRequest;
+        mRangeDataNtfConfig = rangeDataNtfConfig;
+        mRangeDataNtfProximityNear = rangeDataNtfProximityNear;
+        mRangeDataNtfProximityFar = rangeDataNtfProximityFar;
+        mHasTimeOfFlightReport = hasTimeOfFlightReport;
+        mHasAngleOfArrivalAzimuthReport = hasAngleOfArrivalAzimuthReport;
+        mHasAngleOfArrivalElevationReport = hasAngleOfArrivalElevationReport;
+        mHasAngleOfArrivalFigureOfMeritReport = hasAngleOfArrivalFigureOfMeritReport;
+        mAoaType = aoaType;
+        mNumOfMsrmtFocusOnRange = numOfMsrmtFocusOnRange;
+        mNumOfMsrmtFocusOnAoaAzimuth = numOfMsrmtFocusOnAoaAzimuth;
+        mNumOfMsrmtFocusOnAoaElevation = numOfMsrmtFocusOnAoaElevation;
+    }
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    public int getSessionId() {
+        return mSessionId;
+    }
+
+    @RangingDeviceType
+    public int getDeviceType() {
+        return mDeviceType;
+    }
+
+    @RangingDeviceRole
+    public int getDeviceRole() {
+        return mDeviceRole;
+    }
+
+    @RangingRoundUsage
+    public int getRangingRoundUsage() {
+        return mRangingRoundUsage;
+    }
+
+    @MultiNodeMode
+    public int getMultiNodeMode() {
+        return mMultiNodeMode;
+    }
+
+    public UwbAddress getDeviceAddress() {
+        return mDeviceAddress;
+    }
+
+    public List<UwbAddress> getDestAddressList() {
+        return Collections.unmodifiableList(mDestAddressList);
+    }
+
+    public int getInitiationTimeMs() {
+        return mInitiationTimeMs;
+    }
+
+    public int getSlotDurationRstu() {
+        return mSlotDurationRstu;
+    }
+
+    public int getSlotsPerRangingRound() {
+        return mSlotsPerRangingRound;
+    }
+
+    public int getRangingIntervalMs() {
+        return mRangingIntervalMs;
+    }
+
+    public int getBlockStrideLength() {
+        return mBlockStrideLength;
+    }
+
+    public int getHoppingMode() {
+        return mHoppingMode;
+    }
+
+    @IntRange(from = 0, to = 65535)
+    public int getMaxRangingRoundRetries() {
+        return mMaxRangingRoundRetries;
+    }
+
+    public int getSessionPriority() {
+        return mSessionPriority;
+    }
+
+    @MacAddressMode
+    public int getMacAddressMode() {
+        return mMacAddressMode;
+    }
+
+    public boolean hasResultReportPhase() {
+        return mHasResultReportPhase;
+    }
+
+    @MeasurementReportType
+    public int getMeasurementReportType() {
+        return mMeasurementReportType;
+    }
+
+    @IntRange(from = 1, to = 10)
+    public int getInBandTerminationAttemptCount() {
+        return mInBandTerminationAttemptCount;
+    }
+
+    @UwbChannel
+    public int getChannelNumber() {
+        return mChannelNumber;
+    }
+
+    public int getPreambleCodeIndex() {
+        return mPreambleCodeIndex;
+    }
+
+    @RframeConfig
+    public int getRframeConfig() {
+        return mRframeConfig;
+    }
+
+    @PrfMode
+    public int getPrfMode() {
+        return mPrfMode;
+    }
+
+    @PreambleDuration
+    public int getPreambleDuration() {
+        return mPreambleDuration;
+    }
+
+    @SfdIdValue
+    public int getSfdId() {
+        return mSfdId;
+    }
+
+    @StsSegmentCountValue
+    public int getStsSegmentCount() {
+        return mStsSegmentCount;
+    }
+
+    @StsLength
+    public int getStsLength() {
+        return mStsLength;
+    }
+
+    @PsduDataRate
+    public int getPsduDataRate() {
+        return mPsduDataRate;
+    }
+
+    @BprfPhrDataRate
+    public int getBprfPhrDataRate() {
+        return mBprfPhrDataRate;
+    }
+
+    @MacFcsType
+    public int getFcsType() {
+        return mFcsType;
+    }
+
+    public boolean isTxAdaptivePayloadPowerEnabled() {
+        return mIsTxAdaptivePayloadPowerEnabled;
+    }
+
+    @StsConfig
+    public int getStsConfig() {
+        return mStsConfig;
+    }
+
+    public int getSubSessionId() {
+        return mSubSessionId;
+    }
+
+    @Nullable
+    public byte[] getVendorId() {
+        return mVendorId;
+    }
+
+    @Nullable
+    public byte[] getStaticStsIV() {
+        return mStaticStsIV;
+    }
+
+    public boolean isKeyRotationEnabled() {
+        return mIsKeyRotationEnabled;
+    }
+
+    public int getKeyRotationRate() {
+        return mKeyRotationRate;
+    }
+
+    @AoaResultRequestMode
+    public int getAoaResultRequest() {
+        return mAoaResultRequest;
+    }
+
+    @RangeDataNtfConfig
+    public int getRangeDataNtfConfig() {
+        return mRangeDataNtfConfig;
+    }
+
+    public int getRangeDataNtfProximityNear() {
+        return mRangeDataNtfProximityNear;
+    }
+
+    public int getRangeDataNtfProximityFar() {
+        return mRangeDataNtfProximityFar;
+    }
+
+    public boolean hasTimeOfFlightReport() {
+        return mHasTimeOfFlightReport;
+    }
+
+    public boolean hasAngleOfArrivalAzimuthReport() {
+        return mHasAngleOfArrivalAzimuthReport;
+    }
+
+    public boolean hasAngleOfArrivalElevationReport() {
+        return mHasAngleOfArrivalElevationReport;
+    }
+
+    public boolean hasAngleOfArrivalFigureOfMeritReport() {
+        return mHasAngleOfArrivalFigureOfMeritReport;
+    }
+
+    @AoaType
+    public int getAoaType() {
+        return mAoaType;
+    }
+
+    public int getNumOfMsrmtFocusOnRange() {
+        return mNumOfMsrmtFocusOnRange;
+    }
+
+    public int getNumOfMsrmtFocusOnAoaAzimuth() {
+        return mNumOfMsrmtFocusOnAoaAzimuth;
+    }
+
+    public int getNumOfMsrmtFocusOnAoaElevation() {
+        return mNumOfMsrmtFocusOnAoaElevation;
+    }
+
+    @Nullable
+    private static int[] byteArrayToIntArray(@Nullable byte[] bytes) {
+        if (bytes == null) {
+            return null;
+        }
+
+        int[] values = new int[bytes.length];
+        for (int i = 0; i < values.length; i++) {
+            values[i] = bytes[i];
+        }
+        return values;
+    }
+
+    @Nullable
+    private static byte[] intArrayToByteArray(@Nullable int[] values) {
+        if (values == null) {
+            return null;
+        }
+        byte[] bytes = new byte[values.length];
+        for (int i = 0; i < values.length; i++) {
+            bytes[i] = (byte) values[i];
+        }
+        return bytes;
+    }
+
+    @Override
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = super.toBundle();
+        bundle.putString(KEY_PROTOCOL_VERSION, mProtocolVersion.toString());
+        bundle.putInt(KEY_SESSION_ID, mSessionId);
+        bundle.putInt(KEY_DEVICE_TYPE, mDeviceType);
+        bundle.putInt(KEY_DEVICE_ROLE, mDeviceRole);
+        bundle.putInt(KEY_RANGING_ROUND_USAGE, mRangingRoundUsage);
+        bundle.putInt(KEY_MULTI_NODE_MODE, mMultiNodeMode);
+        // Always store address as long in bundle.
+        bundle.putLong(KEY_DEVICE_ADDRESS, uwbAddressToLong(mDeviceAddress));
+
+        // Dest Address list needs to be converted to long array.
+        long[] destAddressList = new long[mDestAddressList.size()];
+        int i = 0;
+        for (UwbAddress destAddress : mDestAddressList) {
+            destAddressList[i++] = uwbAddressToLong(destAddress);
+        }
+        bundle.putLongArray(KEY_DEST_ADDRESS_LIST, destAddressList);
+
+        bundle.putInt(KEY_INITIATION_TIME_MS, mInitiationTimeMs);
+        bundle.putInt(KEY_SLOT_DURATION_RSTU, mSlotDurationRstu);
+        bundle.putInt(KEY_SLOTS_PER_RANGING_ROUND, mSlotsPerRangingRound);
+        bundle.putInt(KEY_RANGING_INTERVAL_MS, mRangingIntervalMs);
+        bundle.putInt(KEY_BLOCK_STRIDE_LENGTH, mBlockStrideLength);
+        bundle.putInt(KEY_HOPPING_MODE, mHoppingMode);
+        bundle.putInt(KEY_MAX_RANGING_ROUND_RETRIES, mMaxRangingRoundRetries);
+        bundle.putInt(KEY_SESSION_PRIORITY, mSessionPriority);
+        bundle.putInt(KEY_MAC_ADDRESS_MODE, mMacAddressMode);
+        bundle.putBoolean(KEY_HAS_RESULT_REPORT_PHASE, mHasResultReportPhase);
+        bundle.putInt(KEY_MEASUREMENT_REPORT_TYPE, mMeasurementReportType);
+        bundle.putInt(KEY_IN_BAND_TERMINATION_ATTEMPT_COUNT, mInBandTerminationAttemptCount);
+        bundle.putInt(KEY_CHANNEL_NUMBER, mChannelNumber);
+        bundle.putInt(KEY_PREAMBLE_CODE_INDEX, mPreambleCodeIndex);
+        bundle.putInt(KEY_RFRAME_CONFIG, mRframeConfig);
+        bundle.putInt(KEY_PRF_MODE, mPrfMode);
+        bundle.putInt(KEY_PREAMBLE_DURATION, mPreambleDuration);
+        bundle.putInt(KEY_SFD_ID, mSfdId);
+        bundle.putInt(KEY_STS_SEGMENT_COUNT, mStsSegmentCount);
+        bundle.putInt(KEY_STS_LENGTH, mStsLength);
+        bundle.putInt(KEY_PSDU_DATA_RATE, mPsduDataRate);
+        bundle.putInt(KEY_BPRF_PHR_DATA_RATE, mBprfPhrDataRate);
+        bundle.putInt(KEY_FCS_TYPE, mFcsType);
+        bundle.putBoolean(
+                KEY_IS_TX_ADAPTIVE_PAYLOAD_POWER_ENABLED, mIsTxAdaptivePayloadPowerEnabled);
+        bundle.putInt(KEY_STS_CONFIG, mStsConfig);
+        if (mStsConfig == STS_CONFIG_DYNAMIC_FOR_CONTROLEE_INDIVIDUAL_KEY) {
+            bundle.putInt(KEY_SUB_SESSION_ID, mSubSessionId);
+        }
+        bundle.putIntArray(KEY_VENDOR_ID, byteArrayToIntArray(mVendorId));
+        bundle.putIntArray(KEY_STATIC_STS_IV, byteArrayToIntArray(mStaticStsIV));
+        bundle.putBoolean(KEY_IS_KEY_ROTATION_ENABLED, mIsKeyRotationEnabled);
+        bundle.putInt(KEY_KEY_ROTATION_RATE, mKeyRotationRate);
+        bundle.putInt(KEY_AOA_RESULT_REQUEST, mAoaResultRequest);
+        bundle.putInt(KEY_RANGE_DATA_NTF_CONFIG, mRangeDataNtfConfig);
+        bundle.putInt(KEY_RANGE_DATA_NTF_PROXIMITY_NEAR, mRangeDataNtfProximityNear);
+        bundle.putInt(KEY_RANGE_DATA_NTF_PROXIMITY_FAR, mRangeDataNtfProximityFar);
+        bundle.putBoolean(KEY_HAS_TIME_OF_FLIGHT_REPORT, mHasTimeOfFlightReport);
+        bundle.putBoolean(KEY_HAS_ANGLE_OF_ARRIVAL_AZIMUTH_REPORT, mHasAngleOfArrivalAzimuthReport);
+        bundle.putBoolean(
+                KEY_HAS_ANGLE_OF_ARRIVAL_ELEVATION_REPORT, mHasAngleOfArrivalElevationReport);
+        bundle.putBoolean(
+                KEY_HAS_ANGLE_OF_ARRIVAL_FIGURE_OF_MERIT_REPORT,
+                mHasAngleOfArrivalFigureOfMeritReport);
+        bundle.putInt(KEY_AOA_TYPE, mAoaType);
+        bundle.putInt(KEY_NUM_OF_MSRMT_FOCUS_ON_RANGE, mNumOfMsrmtFocusOnRange);
+        bundle.putInt(KEY_NUM_OF_MSRMT_FOCUS_ON_AOA_AZIMUTH, mNumOfMsrmtFocusOnAoaAzimuth);
+        bundle.putInt(KEY_NUM_OF_MSRMT_FOCUS_ON_AOA_ELEVATION, mNumOfMsrmtFocusOnAoaElevation);
+        return bundle;
+    }
+
+    public static FiraOpenSessionParams fromBundle(PersistableBundle bundle) {
+        if (!isCorrectProtocol(bundle)) {
+            throw new IllegalArgumentException("Invalid protocol");
+        }
+
+        switch (getBundleVersion(bundle)) {
+            case BUNDLE_VERSION_1:
+                return parseBundleVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("unknown bundle version");
+        }
+    }
+
+    private static FiraOpenSessionParams parseBundleVersion1(PersistableBundle bundle) {
+        int macAddressMode = bundle.getInt(KEY_MAC_ADDRESS_MODE);
+        int addressByteLength = 2;
+        if (macAddressMode == MAC_ADDRESS_MODE_8_BYTES) {
+            addressByteLength = 8;
+        }
+        UwbAddress deviceAddress =
+                longToUwbAddress(bundle.getLong(KEY_DEVICE_ADDRESS), addressByteLength);
+
+        long[] destAddresses = bundle.getLongArray(KEY_DEST_ADDRESS_LIST);
+        List<UwbAddress> destAddressList = new ArrayList<>();
+        for (long address : destAddresses) {
+            destAddressList.add(longToUwbAddress(address, addressByteLength));
+        }
+
+        return new FiraOpenSessionParams.Builder()
+                .setProtocolVersion(
+                        FiraProtocolVersion.fromString(
+                                requireNonNull(bundle.getString(KEY_PROTOCOL_VERSION))))
+                .setSessionId(bundle.getInt(KEY_SESSION_ID))
+                .setDeviceType(bundle.getInt(KEY_DEVICE_TYPE))
+                .setDeviceRole(bundle.getInt(KEY_DEVICE_ROLE))
+                .setRangingRoundUsage(bundle.getInt(KEY_RANGING_ROUND_USAGE))
+                .setMultiNodeMode(bundle.getInt(KEY_MULTI_NODE_MODE))
+                .setDeviceAddress(deviceAddress)
+                .setDestAddressList(destAddressList)
+                .setInitiationTimeMs(bundle.getInt(KEY_INITIATION_TIME_MS))
+                .setSlotDurationRstu(bundle.getInt(KEY_SLOT_DURATION_RSTU))
+                .setSlotsPerRangingRound(bundle.getInt(KEY_SLOTS_PER_RANGING_ROUND))
+                .setRangingIntervalMs(bundle.getInt(KEY_RANGING_INTERVAL_MS))
+                .setBlockStrideLength(bundle.getInt(KEY_BLOCK_STRIDE_LENGTH))
+                .setHoppingMode(bundle.getInt(KEY_HOPPING_MODE))
+                .setMaxRangingRoundRetries(bundle.getInt(KEY_MAX_RANGING_ROUND_RETRIES))
+                .setSessionPriority(bundle.getInt(KEY_SESSION_PRIORITY))
+                .setMacAddressMode(bundle.getInt(KEY_MAC_ADDRESS_MODE))
+                .setHasResultReportPhase(bundle.getBoolean(KEY_HAS_RESULT_REPORT_PHASE))
+                .setMeasurementReportType(bundle.getInt(KEY_MEASUREMENT_REPORT_TYPE))
+                .setInBandTerminationAttemptCount(
+                        bundle.getInt(KEY_IN_BAND_TERMINATION_ATTEMPT_COUNT))
+                .setChannelNumber(bundle.getInt(KEY_CHANNEL_NUMBER))
+                .setPreambleCodeIndex(bundle.getInt(KEY_PREAMBLE_CODE_INDEX))
+                .setRframeConfig(bundle.getInt(KEY_RFRAME_CONFIG))
+                .setPrfMode(bundle.getInt(KEY_PRF_MODE))
+                .setPreambleDuration(bundle.getInt(KEY_PREAMBLE_DURATION))
+                .setSfdId(bundle.getInt(KEY_SFD_ID))
+                .setStsSegmentCount(bundle.getInt(KEY_STS_SEGMENT_COUNT))
+                .setStsLength(bundle.getInt(KEY_STS_LENGTH))
+                .setPsduDataRate(bundle.getInt(KEY_PSDU_DATA_RATE))
+                .setBprfPhrDataRate(bundle.getInt(KEY_BPRF_PHR_DATA_RATE))
+                .setFcsType(bundle.getInt(KEY_FCS_TYPE))
+                .setIsTxAdaptivePayloadPowerEnabled(
+                        bundle.getBoolean(KEY_IS_TX_ADAPTIVE_PAYLOAD_POWER_ENABLED))
+                .setStsConfig(bundle.getInt(KEY_STS_CONFIG))
+                .setSubSessionId(bundle.getInt(KEY_SUB_SESSION_ID))
+                .setVendorId(intArrayToByteArray(bundle.getIntArray(KEY_VENDOR_ID)))
+                .setStaticStsIV(intArrayToByteArray(bundle.getIntArray(KEY_STATIC_STS_IV)))
+                .setIsKeyRotationEnabled(bundle.getBoolean(KEY_IS_KEY_ROTATION_ENABLED))
+                .setKeyRotationRate(bundle.getInt(KEY_KEY_ROTATION_RATE))
+                .setAoaResultRequest(bundle.getInt(KEY_AOA_RESULT_REQUEST))
+                .setRangeDataNtfConfig(bundle.getInt(KEY_RANGE_DATA_NTF_CONFIG))
+                .setRangeDataNtfProximityNear(bundle.getInt(KEY_RANGE_DATA_NTF_PROXIMITY_NEAR))
+                .setRangeDataNtfProximityFar(bundle.getInt(KEY_RANGE_DATA_NTF_PROXIMITY_FAR))
+                .setHasTimeOfFlightReport(bundle.getBoolean(KEY_HAS_TIME_OF_FLIGHT_REPORT))
+                .setHasAngleOfArrivalAzimuthReport(
+                        bundle.getBoolean(KEY_HAS_ANGLE_OF_ARRIVAL_AZIMUTH_REPORT))
+                .setHasAngleOfArrivalElevationReport(
+                        bundle.getBoolean(KEY_HAS_ANGLE_OF_ARRIVAL_ELEVATION_REPORT))
+                .setHasAngleOfArrivalFigureOfMeritReport(
+                        bundle.getBoolean(KEY_HAS_ANGLE_OF_ARRIVAL_FIGURE_OF_MERIT_REPORT))
+                .setAoaType(bundle.getInt(KEY_AOA_TYPE))
+                .setMeasurementFocusRatio(
+                        bundle.getInt(KEY_NUM_OF_MSRMT_FOCUS_ON_RANGE),
+                        bundle.getInt(KEY_NUM_OF_MSRMT_FOCUS_ON_AOA_AZIMUTH),
+                        bundle.getInt(KEY_NUM_OF_MSRMT_FOCUS_ON_AOA_ELEVATION))
+                .build();
+    }
+
+    public FiraProtocolVersion getProtocolVersion() {
+        return mProtocolVersion;
+    }
+
+    /** Builder */
+    public static final class Builder {
+        private final RequiredParam<FiraProtocolVersion> mProtocolVersion = new RequiredParam<>();
+
+        private final RequiredParam<Integer> mSessionId = new RequiredParam<>();
+        private final RequiredParam<Integer> mDeviceType = new RequiredParam<>();
+        private final RequiredParam<Integer> mDeviceRole = new RequiredParam<>();
+
+        /** UCI spec default: DS-TWR with deferred mode */
+        @RangingRoundUsage
+        private int mRangingRoundUsage = RANGING_ROUND_USAGE_DS_TWR_DEFERRED_MODE;
+
+        private final RequiredParam<Integer> mMultiNodeMode = new RequiredParam<>();
+        private UwbAddress mDeviceAddress = null;
+        private List<UwbAddress> mDestAddressList = null;
+
+        /** UCI spec default: 0ms */
+        private int mInitiationTimeMs = 0;
+
+        /** UCI spec default: 2400 RSTU (2 ms). */
+        private int mSlotDurationRstu = 2400;
+
+        /** UCI spec default: 30 slots per ranging round. */
+        private int mSlotsPerRangingRound = 30;
+
+        /** UCI spec default: RANGING_INTERVAL 200 ms */
+        private int mRangingIntervalMs = 200;
+
+        /** UCI spec default: no block striding. */
+        private int mBlockStrideLength = 0;
+
+        /** UCI spec default: no hopping. */
+        private int mHoppingMode = HOPPING_MODE_DISABLE;
+
+        /** UCI spec default: Termination is disabled and ranging round attempt is infinite */
+        @IntRange(from = 0, to = 65535)
+        private int mMaxRangingRoundRetries = 0;
+
+        /** UCI spec default: priority 50 */
+        private int mSessionPriority = 50;
+
+        /** UCI spec default: 2-byte short address */
+        @MacAddressMode private int mMacAddressMode = MAC_ADDRESS_MODE_2_BYTES;
+
+        /** UCI spec default: RANGING_ROUND_CONTROL bit 0 default 1 */
+        private boolean mHasResultReportPhase = true;
+
+        /** UCI spec default: RANGING_ROUND_CONTROL bit 7 default 0 */
+        @MeasurementReportType
+        private int mMeasurementReportType = MEASUREMENT_REPORT_TYPE_INITIATOR_TO_RESPONDER;
+
+        /** UCI spec default: in-band termination signal will be sent once. */
+        @IntRange(from = 1, to = 10)
+        private int mInBandTerminationAttemptCount = 1;
+
+        /** UCI spec default: Channel 9, which is the only mandatory channel. */
+        @UwbChannel private int mChannelNumber = UWB_CHANNEL_9;
+
+        /** UCI spec default: index 10 */
+        @UwbPreambleCodeIndex private int mPreambleCodeIndex = UWB_PREAMBLE_CODE_INDEX_10;
+
+        /** UCI spec default: SP3 */
+        private int mRframeConfig = RFRAME_CONFIG_SP3;
+
+        /** UCI spec default: BPRF */
+        @PrfMode private int mPrfMode = PRF_MODE_BPRF;
+
+        /** UCI spec default: 64 symbols */
+        @PreambleDuration private int mPreambleDuration = PREAMBLE_DURATION_T64_SYMBOLS;
+
+        /** UCI spec default: ID 2 */
+        @SfdIdValue private int mSfdId = SFD_ID_VALUE_2;
+
+        /** UCI spec default: one STS segment */
+        @StsSegmentCountValue private int mStsSegmentCount = STS_SEGMENT_COUNT_VALUE_1;
+
+        /** UCI spec default: 64 symbols */
+        @StsLength private int mStsLength = STS_LENGTH_64_SYMBOLS;
+
+        /** UCI spec default: 6.81Mb/s */
+        @PsduDataRate private int mPsduDataRate = PSDU_DATA_RATE_6M81;
+
+        /** UCI spec default: 850kb/s */
+        @BprfPhrDataRate private int mBprfPhrDataRate = BPRF_PHR_DATA_RATE_850K;
+
+        /** UCI spec default: CRC-16 */
+        @MacFcsType private int mFcsType = MAC_FCS_TYPE_CRC_16;
+
+        /** UCI spec default: adaptive payload power for TX disabled */
+        private boolean mIsTxAdaptivePayloadPowerEnabled = false;
+
+        /** UCI spec default: static STS */
+        @StsConfig private int mStsConfig = STS_CONFIG_STATIC;
+
+        /**
+         * Per UCI spec, only required when STS config is
+         * STS_CONFIG_DYNAMIC_FOR_CONTROLEE_INDIVIDUAL_KEY.
+         */
+        private final RequiredParam<Integer> mSubSessionId = new RequiredParam<>();
+
+        /** STATIC STS only. For Key generation. 16-bit long */
+        @Nullable private byte[] mVendorId = null;
+
+        /** STATIC STS only. For Key generation. 48-bit long */
+        @Nullable private byte[] mStaticStsIV = null;
+
+        /** UCI spec default: no key rotation */
+        private boolean mIsKeyRotationEnabled = false;
+
+        /** UCI spec default: 0 */
+        private int mKeyRotationRate = 0;
+
+        /** UCI spec default: AoA enabled. */
+        @AoaResultRequestMode
+        private int mAoaResultRequest = AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS;
+
+        /** UCI spec default: Ranging notification enabled. */
+        @RangeDataNtfConfig private int mRangeDataNtfConfig = RANGE_DATA_NTF_CONFIG_ENABLE;
+
+        /** UCI spec default: 0 (No low-bound filtering) */
+        private int mRangeDataNtfProximityNear = 0;
+
+        /** UCI spec default: 20000 cm (or 200 meters) */
+        private int mRangeDataNtfProximityFar = 20000;
+
+        /** UCI spec default: RESULT_REPORT_CONFIG bit 0 is 1 */
+        private boolean mHasTimeOfFlightReport = true;
+
+        /** UCI spec default: RESULT_REPORT_CONFIG bit 1 is 0 */
+        private boolean mHasAngleOfArrivalAzimuthReport = false;
+
+        /** UCI spec default: RESULT_REPORT_CONFIG bit 2 is 0 */
+        private boolean mHasAngleOfArrivalElevationReport = false;
+
+        /** UCI spec default: RESULT_REPORT_CONFIG bit 3 is 0 */
+        private boolean mHasAngleOfArrivalFigureOfMeritReport = false;
+
+        /** Not defined in UCI, we use Azimuth-only as default */
+        @AoaType private int mAoaType = AOA_TYPE_AZIMUTH;
+
+        /** Interleaving ratios are not set by default */
+        private int mNumOfMsrmtFocusOnRange = 0;
+        private int mNumOfMsrmtFocusOnAoaAzimuth = 0;
+        private int mNumOfMsrmtFocusOnAoaElevation = 0;
+
+        public Builder() {}
+
+        public Builder(@NonNull Builder builder) {
+            mProtocolVersion.set(builder.mProtocolVersion.get());
+            mSessionId.set(builder.mSessionId.get());
+            mDeviceType.set(builder.mDeviceType.get());
+            mDeviceRole.set(builder.mDeviceRole.get());
+            mRangingRoundUsage = builder.mRangingRoundUsage;
+            mMultiNodeMode.set(builder.mMultiNodeMode.get());
+            mDeviceAddress = builder.mDeviceAddress;
+            mDestAddressList = builder.mDestAddressList;
+            mInitiationTimeMs = builder.mInitiationTimeMs;
+            mSlotDurationRstu = builder.mSlotDurationRstu;
+            mSlotsPerRangingRound = builder.mSlotsPerRangingRound;
+            mRangingIntervalMs = builder.mRangingIntervalMs;
+            mBlockStrideLength = builder.mBlockStrideLength;
+            mHoppingMode = builder.mHoppingMode;
+            mMaxRangingRoundRetries = builder.mMaxRangingRoundRetries;
+            mSessionPriority = builder.mSessionPriority;
+            mMacAddressMode = builder.mMacAddressMode;
+            mHasResultReportPhase = builder.mHasResultReportPhase;
+            mMeasurementReportType = builder.mMeasurementReportType;
+            mInBandTerminationAttemptCount = builder.mInBandTerminationAttemptCount;
+            mChannelNumber = builder.mChannelNumber;
+            mPreambleCodeIndex = builder.mPreambleCodeIndex;
+            mRframeConfig = builder.mRframeConfig;
+            mPrfMode = builder.mPrfMode;
+            mPreambleDuration = builder.mPreambleDuration;
+            mSfdId = builder.mSfdId;
+            mStsSegmentCount = builder.mStsSegmentCount;
+            mStsLength = builder.mStsLength;
+            mPsduDataRate = builder.mPsduDataRate;
+            mBprfPhrDataRate = builder.mBprfPhrDataRate;
+            mFcsType = builder.mFcsType;
+            mIsTxAdaptivePayloadPowerEnabled = builder.mIsTxAdaptivePayloadPowerEnabled;
+            mStsConfig = builder.mStsConfig;
+            if (builder.mSubSessionId.isSet()) mSubSessionId.set(builder.mSubSessionId.get());
+            mVendorId = builder.mVendorId;
+            mStaticStsIV = builder.mStaticStsIV;
+            mIsKeyRotationEnabled = builder.mIsKeyRotationEnabled;
+            mKeyRotationRate = builder.mKeyRotationRate;
+            mAoaResultRequest = builder.mAoaResultRequest;
+            mRangeDataNtfConfig = builder.mRangeDataNtfConfig;
+            mRangeDataNtfProximityNear = builder.mRangeDataNtfProximityNear;
+            mRangeDataNtfProximityFar = builder.mRangeDataNtfProximityFar;
+            mHasTimeOfFlightReport = builder.mHasTimeOfFlightReport;
+            mHasAngleOfArrivalAzimuthReport = builder.mHasAngleOfArrivalAzimuthReport;
+            mHasAngleOfArrivalElevationReport = builder.mHasAngleOfArrivalElevationReport;
+            mHasAngleOfArrivalFigureOfMeritReport = builder.mHasAngleOfArrivalFigureOfMeritReport;
+            mAoaType = builder.mAoaType;
+        }
+
+        public FiraOpenSessionParams.Builder setProtocolVersion(FiraProtocolVersion version) {
+            mProtocolVersion.set(version);
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setSessionId(int sessionId) {
+            mSessionId.set(sessionId);
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setDeviceType(@RangingDeviceType int deviceType) {
+            mDeviceType.set(deviceType);
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setDeviceRole(@RangingDeviceRole int deviceRole) {
+            mDeviceRole.set(deviceRole);
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setRangingRoundUsage(
+                @RangingRoundUsage int rangingRoundUsage) {
+            mRangingRoundUsage = rangingRoundUsage;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setMultiNodeMode(@MultiNodeMode int multiNodeMode) {
+            mMultiNodeMode.set(multiNodeMode);
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setDeviceAddress(UwbAddress deviceAddress) {
+            mDeviceAddress = deviceAddress;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setDestAddressList(List<UwbAddress> destAddressList) {
+            mDestAddressList = destAddressList;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setInitiationTimeMs(int initiationTimeMs) {
+            mInitiationTimeMs = initiationTimeMs;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setSlotDurationRstu(int slotDurationRstu) {
+            mSlotDurationRstu = slotDurationRstu;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setSlotsPerRangingRound(int slotsPerRangingRound) {
+            mSlotsPerRangingRound = slotsPerRangingRound;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setRangingIntervalMs(int rangingIntervalMs) {
+            mRangingIntervalMs = rangingIntervalMs;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setBlockStrideLength(int blockStrideLength) {
+            mBlockStrideLength = blockStrideLength;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setHoppingMode(int hoppingMode) {
+            this.mHoppingMode = hoppingMode;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setMaxRangingRoundRetries(
+                @IntRange(from = 0, to = 65535) int maxRangingRoundRetries) {
+            mMaxRangingRoundRetries = maxRangingRoundRetries;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setSessionPriority(int sessionPriority) {
+            mSessionPriority = sessionPriority;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setMacAddressMode(int macAddressMode) {
+            this.mMacAddressMode = macAddressMode;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setHasResultReportPhase(boolean hasResultReportPhase) {
+            mHasResultReportPhase = hasResultReportPhase;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setMeasurementReportType(
+                @MeasurementReportType int measurementReportType) {
+            mMeasurementReportType = measurementReportType;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setInBandTerminationAttemptCount(
+                @IntRange(from = 1, to = 10) int inBandTerminationAttemptCount) {
+            mInBandTerminationAttemptCount = inBandTerminationAttemptCount;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setChannelNumber(@UwbChannel int channelNumber) {
+            mChannelNumber = channelNumber;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setPreambleCodeIndex(
+                @UwbPreambleCodeIndex int preambleCodeIndex) {
+            mPreambleCodeIndex = preambleCodeIndex;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setRframeConfig(@RframeConfig int rframeConfig) {
+            mRframeConfig = rframeConfig;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setPrfMode(@PrfMode int prfMode) {
+            mPrfMode = prfMode;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setPreambleDuration(
+                @PreambleDuration int preambleDuration) {
+            mPreambleDuration = preambleDuration;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setSfdId(@SfdIdValue int sfdId) {
+            mSfdId = sfdId;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setStsSegmentCount(
+                @StsSegmentCountValue int stsSegmentCount) {
+            mStsSegmentCount = stsSegmentCount;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setStsLength(@StsLength int stsLength) {
+            mStsLength = stsLength;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setPsduDataRate(@PsduDataRate int psduDataRate) {
+            mPsduDataRate = psduDataRate;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setBprfPhrDataRate(
+                @BprfPhrDataRate int bprfPhrDataRate) {
+            mBprfPhrDataRate = bprfPhrDataRate;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setFcsType(@MacFcsType int fcsType) {
+            mFcsType = fcsType;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setIsTxAdaptivePayloadPowerEnabled(
+                boolean isTxAdaptivePayloadPowerEnabled) {
+            mIsTxAdaptivePayloadPowerEnabled = isTxAdaptivePayloadPowerEnabled;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setStsConfig(@StsConfig int stsConfig) {
+            mStsConfig = stsConfig;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setSubSessionId(int subSessionId) {
+            mSubSessionId.set(subSessionId);
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setVendorId(@Nullable byte[] vendorId) {
+            mVendorId = vendorId;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setStaticStsIV(@Nullable byte[] staticStsIV) {
+            mStaticStsIV = staticStsIV;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setIsKeyRotationEnabled(boolean isKeyRotationEnabled) {
+            mIsKeyRotationEnabled = isKeyRotationEnabled;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setKeyRotationRate(int keyRotationRate) {
+            mKeyRotationRate = keyRotationRate;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setAoaResultRequest(
+                @AoaResultRequestMode int aoaResultRequest) {
+            mAoaResultRequest = aoaResultRequest;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setRangeDataNtfConfig(
+                @RangeDataNtfConfig int rangeDataNtfConfig) {
+            mRangeDataNtfConfig = rangeDataNtfConfig;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setRangeDataNtfProximityNear(
+                int rangeDataNtfProximityNear) {
+            mRangeDataNtfProximityNear = rangeDataNtfProximityNear;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setRangeDataNtfProximityFar(
+                int rangeDataNtfProximityFar) {
+            mRangeDataNtfProximityFar = rangeDataNtfProximityFar;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setHasTimeOfFlightReport(
+                boolean hasTimeOfFlightReport) {
+            mHasTimeOfFlightReport = hasTimeOfFlightReport;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setHasAngleOfArrivalAzimuthReport(
+                boolean hasAngleOfArrivalAzimuthReport) {
+            mHasAngleOfArrivalAzimuthReport = hasAngleOfArrivalAzimuthReport;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setHasAngleOfArrivalElevationReport(
+                boolean hasAngleOfArrivalElevationReport) {
+            mHasAngleOfArrivalElevationReport = hasAngleOfArrivalElevationReport;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setHasAngleOfArrivalFigureOfMeritReport(
+                boolean hasAngleOfArrivalFigureOfMeritReport) {
+            mHasAngleOfArrivalFigureOfMeritReport = hasAngleOfArrivalFigureOfMeritReport;
+            return this;
+        }
+
+        public FiraOpenSessionParams.Builder setAoaType(int aoaType) {
+            mAoaType = aoaType;
+            return this;
+        }
+
+       /**
+        * After the session has been started, the device starts by
+        * performing numOfMsrmtFocusOnRange range-only measurements (no
+        * AoA), then it proceeds with numOfMsrmtFocusOnAoaAzimuth AoA
+        * azimuth measurements followed by numOfMsrmtFocusOnAoaElevation
+        * AoA elevation measurements.
+        * If this is not invoked, the focus of each measurement is left
+        * to the UWB vendor.
+        *
+        * Only valid when {@link #setAoaResultRequest(int)} is set to
+        * {@link FiraParams#AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_INTERLEAVED}.
+        */
+        public FiraOpenSessionParams.Builder setMeasurementFocusRatio(
+                int numOfMsrmtFocusOnRange,
+                int numOfMsrmtFocusOnAoaAzimuth,
+                int numOfMsrmtFocusOnAoaElevation) {
+            mNumOfMsrmtFocusOnRange = numOfMsrmtFocusOnRange;
+            mNumOfMsrmtFocusOnAoaAzimuth = numOfMsrmtFocusOnAoaAzimuth;
+            mNumOfMsrmtFocusOnAoaElevation = numOfMsrmtFocusOnAoaElevation;
+            return this;
+        }
+
+        private void checkAddress() {
+            checkArgument(
+                    mMacAddressMode == MAC_ADDRESS_MODE_2_BYTES
+                            || mMacAddressMode == MAC_ADDRESS_MODE_8_BYTES);
+            int addressByteLength = UwbAddress.SHORT_ADDRESS_BYTE_LENGTH;
+            if (mMacAddressMode == MAC_ADDRESS_MODE_8_BYTES) {
+                addressByteLength = UwbAddress.EXTENDED_ADDRESS_BYTE_LENGTH;
+            }
+
+            // Make sure address length matches the address mode
+            checkArgument(mDeviceAddress != null && mDeviceAddress.size() == addressByteLength);
+            checkNotNull(mDestAddressList);
+            for (UwbAddress destAddress : mDestAddressList) {
+                checkArgument(destAddress != null && destAddress.size() == addressByteLength);
+            }
+        }
+
+        private void checkStsConfig() {
+            if (mStsConfig == STS_CONFIG_STATIC) {
+                // These two fields are used by Static STS only.
+                checkArgument(mVendorId != null && mVendorId.length == 2);
+                checkArgument(mStaticStsIV != null && mStaticStsIV.length == 6);
+            }
+
+            if (mStsConfig != STS_CONFIG_DYNAMIC_FOR_CONTROLEE_INDIVIDUAL_KEY) {
+                // Sub Session ID is used for dynamic individual key STS only.
+                if (!mSubSessionId.isSet()) {
+                    mSubSessionId.set(0);
+                }
+            }
+        }
+
+        private void checkInterleavingRatio() {
+            if (mAoaResultRequest != AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_INTERLEAVED) {
+                checkArgument(mNumOfMsrmtFocusOnRange == 0);
+                checkArgument(mNumOfMsrmtFocusOnAoaAzimuth == 0);
+                checkArgument(mNumOfMsrmtFocusOnAoaElevation == 0);
+            } else {
+                // at-least one of the ratio params should be set for interleaving mode.
+                checkArgument(mNumOfMsrmtFocusOnRange > 0
+                        || mNumOfMsrmtFocusOnAoaAzimuth > 0
+                        || mNumOfMsrmtFocusOnAoaElevation > 0);
+            }
+        }
+
+        public FiraOpenSessionParams build() {
+            checkAddress();
+            checkStsConfig();
+            checkInterleavingRatio();
+            return new FiraOpenSessionParams(
+                    mProtocolVersion.get(),
+                    mSessionId.get(),
+                    mDeviceType.get(),
+                    mDeviceRole.get(),
+                    mRangingRoundUsage,
+                    mMultiNodeMode.get(),
+                    mDeviceAddress,
+                    mDestAddressList,
+                    mInitiationTimeMs,
+                    mSlotDurationRstu,
+                    mSlotsPerRangingRound,
+                    mRangingIntervalMs,
+                    mBlockStrideLength,
+                    mHoppingMode,
+                    mMaxRangingRoundRetries,
+                    mSessionPriority,
+                    mMacAddressMode,
+                    mHasResultReportPhase,
+                    mMeasurementReportType,
+                    mInBandTerminationAttemptCount,
+                    mChannelNumber,
+                    mPreambleCodeIndex,
+                    mRframeConfig,
+                    mPrfMode,
+                    mPreambleDuration,
+                    mSfdId,
+                    mStsSegmentCount,
+                    mStsLength,
+                    mPsduDataRate,
+                    mBprfPhrDataRate,
+                    mFcsType,
+                    mIsTxAdaptivePayloadPowerEnabled,
+                    mStsConfig,
+                    mSubSessionId.get(),
+                    mVendorId,
+                    mStaticStsIV,
+                    mIsKeyRotationEnabled,
+                    mKeyRotationRate,
+                    mAoaResultRequest,
+                    mRangeDataNtfConfig,
+                    mRangeDataNtfProximityNear,
+                    mRangeDataNtfProximityFar,
+                    mHasTimeOfFlightReport,
+                    mHasAngleOfArrivalAzimuthReport,
+                    mHasAngleOfArrivalElevationReport,
+                    mHasAngleOfArrivalFigureOfMeritReport,
+                    mAoaType,
+                    mNumOfMsrmtFocusOnRange,
+                    mNumOfMsrmtFocusOnAoaAzimuth,
+                    mNumOfMsrmtFocusOnAoaElevation);
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/fira/FiraParams.java b/service/support_lib/src/com/google/uwb/support/fira/FiraParams.java
new file mode 100644
index 0000000..c4493cd
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/fira/FiraParams.java
@@ -0,0 +1,762 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.fira;
+
+import android.os.Build.VERSION_CODES;
+import android.os.PersistableBundle;
+import android.uwb.UwbAddress;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RequiresApi;
+
+import com.google.uwb.support.base.FlagEnum;
+import com.google.uwb.support.base.Params;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/** Defines parameters for FiRa operation */
+@RequiresApi(VERSION_CODES.LOLLIPOP)
+public abstract class FiraParams extends Params {
+    public static final String PROTOCOL_NAME = "fira";
+
+    @Override
+    public final String getProtocolName() {
+        return PROTOCOL_NAME;
+    }
+
+    public static boolean isCorrectProtocol(PersistableBundle bundle) {
+        return isProtocol(bundle, PROTOCOL_NAME);
+    }
+
+    public static final FiraProtocolVersion PROTOCOL_VERSION_1_1 = new FiraProtocolVersion(1, 1);
+
+    /** Service ID for FiRa profile */
+    @IntDef(
+            value = {
+                    PACS_PROFILE_SERVICE_ID,
+            })
+    public @interface ServiceID {}
+
+    public static final int PACS_PROFILE_SERVICE_ID = 1;
+
+    /** UWB Channel selections */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                UWB_CHANNEL_5,
+                UWB_CHANNEL_6,
+                UWB_CHANNEL_8,
+                UWB_CHANNEL_9,
+                UWB_CHANNEL_10,
+                UWB_CHANNEL_12,
+                UWB_CHANNEL_13,
+                UWB_CHANNEL_14,
+            })
+    public @interface UwbChannel {}
+
+    public static final int UWB_CHANNEL_5 = 5;
+    public static final int UWB_CHANNEL_6 = 6;
+    public static final int UWB_CHANNEL_8 = 8;
+    public static final int UWB_CHANNEL_9 = 9;
+    public static final int UWB_CHANNEL_10 = 10;
+    public static final int UWB_CHANNEL_12 = 12;
+    public static final int UWB_CHANNEL_13 = 13;
+    public static final int UWB_CHANNEL_14 = 14;
+
+    /** UWB Channel selections */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                UWB_PREAMBLE_CODE_INDEX_9,
+                UWB_PREAMBLE_CODE_INDEX_10,
+                UWB_PREAMBLE_CODE_INDEX_11,
+                UWB_PREAMBLE_CODE_INDEX_12,
+                UWB_PREAMBLE_CODE_INDEX_25,
+                UWB_PREAMBLE_CODE_INDEX_26,
+                UWB_PREAMBLE_CODE_INDEX_27,
+                UWB_PREAMBLE_CODE_INDEX_28,
+                UWB_PREAMBLE_CODE_INDEX_29,
+                UWB_PREAMBLE_CODE_INDEX_30,
+                UWB_PREAMBLE_CODE_INDEX_31,
+                UWB_PREAMBLE_CODE_INDEX_32,
+            })
+    public @interface UwbPreambleCodeIndex {}
+
+    public static final int UWB_PREAMBLE_CODE_INDEX_9 = 9;
+    public static final int UWB_PREAMBLE_CODE_INDEX_10 = 10;
+    public static final int UWB_PREAMBLE_CODE_INDEX_11 = 11;
+    public static final int UWB_PREAMBLE_CODE_INDEX_12 = 12;
+    public static final int UWB_PREAMBLE_CODE_INDEX_25 = 25;
+    public static final int UWB_PREAMBLE_CODE_INDEX_26 = 26;
+    public static final int UWB_PREAMBLE_CODE_INDEX_27 = 27;
+    public static final int UWB_PREAMBLE_CODE_INDEX_28 = 28;
+    public static final int UWB_PREAMBLE_CODE_INDEX_29 = 29;
+    public static final int UWB_PREAMBLE_CODE_INDEX_30 = 30;
+    public static final int UWB_PREAMBLE_CODE_INDEX_31 = 31;
+    public static final int UWB_PREAMBLE_CODE_INDEX_32 = 32;
+
+    /** Ranging frame type */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            value = {
+                RFRAME_CONFIG_SP0,
+                RFRAME_CONFIG_SP1,
+                RFRAME_CONFIG_SP3,
+            })
+    public @interface RframeConfig {}
+
+    /** Ranging frame without STS */
+    public static final int RFRAME_CONFIG_SP0 = 0;
+
+    /** Ranging frame with STS following SFD */
+    public static final int RFRAME_CONFIG_SP1 = 1;
+
+    /** Ranging frame with STS following SFD, no data */
+    public static final int RFRAME_CONFIG_SP3 = 3;
+
+    /** Device type defined in FiRa */
+    @IntDef(
+            value = {
+                RANGING_DEVICE_TYPE_CONTROLEE,
+                RANGING_DEVICE_TYPE_CONTROLLER,
+            })
+    public @interface RangingDeviceType {}
+
+    public static final int RANGING_DEVICE_TYPE_CONTROLEE = 0;
+
+    public static final int RANGING_DEVICE_TYPE_CONTROLLER = 1;
+
+    /** Device role defined in FiRa */
+    @IntDef(
+            value = {
+                RANGING_DEVICE_ROLE_RESPONDER,
+                RANGING_DEVICE_ROLE_INITIATOR,
+            })
+    public @interface RangingDeviceRole {}
+
+    public static final int RANGING_DEVICE_ROLE_RESPONDER = 0;
+
+    public static final int RANGING_DEVICE_ROLE_INITIATOR = 1;
+
+    /** Ranging Round Usage */
+    @IntDef(
+            value = {
+                RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE,
+                RANGING_ROUND_USAGE_DS_TWR_DEFERRED_MODE,
+                RANGING_ROUND_USAGE_SS_TWR_NON_DEFERRED_MODE,
+                RANGING_ROUND_USAGE_DS_TWR_NON_DEFERRED_MODE,
+            })
+    public @interface RangingRoundUsage {}
+
+    /** Single-sided two-way ranging, deferred */
+    public static final int RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE = 1;
+
+    /** Double-sided two-way ranging, deferred */
+    public static final int RANGING_ROUND_USAGE_DS_TWR_DEFERRED_MODE = 2;
+
+    /** Single-sided two-way ranging, non-deferred */
+    public static final int RANGING_ROUND_USAGE_SS_TWR_NON_DEFERRED_MODE = 3;
+
+    /** Double-sided two-way ranging, non-deferred */
+    public static final int RANGING_ROUND_USAGE_DS_TWR_NON_DEFERRED_MODE = 4;
+
+    /** Multi-Node mode */
+    @IntDef(
+            value = {
+                MULTI_NODE_MODE_UNICAST,
+                MULTI_NODE_MODE_ONE_TO_MANY,
+                MULTI_NODE_MODE_MANY_TO_MANY,
+            })
+    public @interface MultiNodeMode {}
+
+    public static final int MULTI_NODE_MODE_UNICAST = 0;
+
+    public static final int MULTI_NODE_MODE_ONE_TO_MANY = 1;
+
+    /** Unuported in Fira 1.1 */
+    public static final int MULTI_NODE_MODE_MANY_TO_MANY = 2;
+
+    /** Measurement Report */
+    @IntDef(
+            value = {
+                MEASUREMENT_REPORT_TYPE_INITIATOR_TO_RESPONDER,
+                MEASUREMENT_REPORT_TYPE_RESPONDER_TO_INITIATOR,
+            })
+    public @interface MeasurementReportType {}
+
+    public static final int MEASUREMENT_REPORT_TYPE_INITIATOR_TO_RESPONDER = 0;
+
+    public static final int MEASUREMENT_REPORT_TYPE_RESPONDER_TO_INITIATOR = 1;
+
+    /** PRF Mode */
+    @IntDef(
+            value = {
+                PRF_MODE_BPRF,
+                PRF_MODE_HPRF,
+            })
+    public @interface PrfMode {}
+
+    public static final int PRF_MODE_BPRF = 0;
+
+    public static final int PRF_MODE_HPRF = 1;
+
+    /** Preamble duration: BPRF always uses 64 symbols */
+    @IntDef(
+            value = {
+                PREAMBLE_DURATION_T32_SYMBOLS,
+                PREAMBLE_DURATION_T64_SYMBOLS,
+            })
+    public @interface PreambleDuration {}
+
+    /** HPRF only */
+    public static final int PREAMBLE_DURATION_T32_SYMBOLS = 0;
+
+    public static final int PREAMBLE_DURATION_T64_SYMBOLS = 1;
+
+    /** PSDU data Rate */
+    @IntDef(
+            value = {
+                PSDU_DATA_RATE_6M81,
+                PSDU_DATA_RATE_7M80,
+                PSDU_DATA_RATE_27M2,
+                PSDU_DATA_RATE_31M2,
+            })
+    public @interface PsduDataRate {}
+
+    /** 6.81 Mbps, default BPRF rate */
+    public static final int PSDU_DATA_RATE_6M81 = 0;
+
+    /** 7.80 Mbps, BPRF rate with convolutional encoding K = 7 */
+    public static final int PSDU_DATA_RATE_7M80 = 1;
+
+    /** 27.2 Mbps, default HPRF rate */
+    public static final int PSDU_DATA_RATE_27M2 = 2;
+
+    /** 31.2 Mbps, HPRF rate with convolutional encoding K = 7 */
+    public static final int PSDU_DATA_RATE_31M2 = 3;
+
+    /** BPRF PHY Header data rate */
+    @IntDef(
+            value = {
+                BPRF_PHR_DATA_RATE_850K,
+                BPRF_PHR_DATA_RATE_6M81,
+            })
+    public @interface BprfPhrDataRate {}
+
+    /** 850 kbps */
+    public static final int BPRF_PHR_DATA_RATE_850K = 0;
+
+    /** 6.81 Mbps */
+    public static final int BPRF_PHR_DATA_RATE_6M81 = 1;
+
+    /** MAC FCS type */
+    @IntDef(
+            value = {
+                MAC_FCS_TYPE_CRC_16,
+                MAC_FCS_TYPE_CRC_32,
+            })
+    public @interface MacFcsType {}
+
+    public static final int MAC_FCS_TYPE_CRC_16 = 0;
+    /** HPRF only */
+    public static final int MAC_FCS_TYPE_CRC_32 = 1;
+
+    /** STS Config */
+    @IntDef(
+            value = {
+                STS_CONFIG_STATIC,
+                STS_CONFIG_DYNAMIC,
+                STS_CONFIG_DYNAMIC_FOR_CONTROLEE_INDIVIDUAL_KEY,
+            })
+    public @interface StsConfig {}
+
+    public static final int STS_CONFIG_STATIC = 0;
+
+    public static final int STS_CONFIG_DYNAMIC = 1;
+
+    public static final int STS_CONFIG_DYNAMIC_FOR_CONTROLEE_INDIVIDUAL_KEY = 2;
+
+    /** AoA request */
+    @IntDef(
+            value = {
+                AOA_RESULT_REQUEST_MODE_NO_AOA_REPORT,
+                AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS,
+                AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_AZIMUTH_ONLY,
+                AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_ELEVATION_ONLY,
+                AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_INTERLEAVED,
+            })
+    public @interface AoaResultRequestMode {}
+
+    public static final int AOA_RESULT_REQUEST_MODE_NO_AOA_REPORT = 0;
+
+    public static final int AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS = 1;
+
+    public static final int AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_AZIMUTH_ONLY = 2;
+
+    public static final int AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_ELEVATION_ONLY = 3;
+
+    public static final int AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_INTERLEAVED = 0xF0;
+
+    /** STS Segment count */
+    @IntDef(
+            value = {
+                STS_SEGMENT_COUNT_VALUE_0,
+                STS_SEGMENT_COUNT_VALUE_1,
+                STS_SEGMENT_COUNT_VALUE_2,
+            })
+    public @interface StsSegmentCountValue {}
+
+    public static final int STS_SEGMENT_COUNT_VALUE_0 = 0;
+
+    public static final int STS_SEGMENT_COUNT_VALUE_1 = 1;
+
+    public static final int STS_SEGMENT_COUNT_VALUE_2 = 2;
+
+    /** SFD ID */
+    @IntDef(
+            value = {
+                SFD_ID_VALUE_1,
+                SFD_ID_VALUE_2,
+                SFD_ID_VALUE_3,
+                SFD_ID_VALUE_4,
+            })
+    public @interface SfdIdValue {}
+
+    public static final int SFD_ID_VALUE_1 = 1;
+    public static final int SFD_ID_VALUE_2 = 2;
+    public static final int SFD_ID_VALUE_3 = 3;
+    public static final int SFD_ID_VALUE_4 = 4;
+
+    /**
+     * Hopping mode (Since FiRa supports vendor-specific values. This annotation is not enforced.)
+     */
+    @IntDef(
+            value = {
+                HOPPING_MODE_DISABLE,
+                HOPPING_MODE_FIRA_HOPPING_ENABLE,
+            })
+    public @interface HoppingMode {}
+
+    public static final int HOPPING_MODE_DISABLE = 0;
+    public static final int HOPPING_MODE_FIRA_HOPPING_ENABLE = 1;
+
+    /** STS Length */
+    @IntDef(
+            value = {
+                STS_LENGTH_32_SYMBOLS,
+                STS_LENGTH_64_SYMBOLS,
+                STS_LENGTH_128_SYMBOLS,
+            })
+    public @interface StsLength {}
+
+    public static final int STS_LENGTH_32_SYMBOLS = 0;
+    public static final int STS_LENGTH_64_SYMBOLS = 1;
+    public static final int STS_LENGTH_128_SYMBOLS = 2;
+
+    /** Range Data Notification Config */
+    @IntDef(
+            value = {
+                RANGE_DATA_NTF_CONFIG_DISABLE,
+                RANGE_DATA_NTF_CONFIG_ENABLE,
+                RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY,
+            })
+    public @interface RangeDataNtfConfig {}
+
+    public static final int RANGE_DATA_NTF_CONFIG_DISABLE = 0;
+    public static final int RANGE_DATA_NTF_CONFIG_ENABLE = 1;
+    public static final int RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY = 2;
+
+    /** MAC address mode: short (2 bytes) or extended (8 bytes) */
+    @IntDef(
+            value = {
+                MAC_ADDRESS_MODE_2_BYTES,
+                MAC_ADDRESS_MODE_8_BYTES_2_BYTES_HEADER,
+                MAC_ADDRESS_MODE_8_BYTES,
+            })
+    public @interface MacAddressMode {}
+
+    public static final int MAC_ADDRESS_MODE_2_BYTES = 0;
+
+    /** Not supported by UCI 1.0 */
+    public static final int MAC_ADDRESS_MODE_8_BYTES_2_BYTES_HEADER = 1;
+
+    public static final int MAC_ADDRESS_MODE_8_BYTES = 2;
+
+    /** AoA type is not defined in UCI. This decides what AoA result we want to get */
+    @IntDef(
+            value = {
+                AOA_TYPE_AZIMUTH,
+                AOA_TYPE_ELEVATION,
+                AOA_TYPE_AZIMUTH_AND_ELEVATION,
+            })
+    public @interface AoaType {}
+
+    public static final int AOA_TYPE_AZIMUTH = 0;
+    public static final int AOA_TYPE_ELEVATION = 1;
+
+    /**
+     * How to get both angles is hardware dependent. Some hardware can get both angle in one round,
+     * some needs two rounds.
+     */
+    public static final int AOA_TYPE_AZIMUTH_AND_ELEVATION = 2;
+
+    /** Status codes defined in UCI */
+    @IntDef(
+            value = {
+                STATUS_CODE_OK,
+                STATUS_CODE_REJECTED,
+                STATUS_CODE_FAILED,
+                STATUS_CODE_SYNTAX_ERROR,
+                STATUS_CODE_INVALID_PARAM,
+                STATUS_CODE_INVALID_RANGE,
+                STATUS_CODE_INVALID_MESSAGE_SIZE,
+                STATUS_CODE_UNKNOWN_GID,
+                STATUS_CODE_UNKNOWN_OID,
+                STATUS_CODE_READ_ONLY,
+                STATUS_CODE_COMMAND_RETRY,
+                STATUS_CODE_ERROR_SESSION_NOT_EXIST,
+                STATUS_CODE_ERROR_SESSION_DUPLICATE,
+                STATUS_CODE_ERROR_SESSION_ACTIVE,
+                STATUS_CODE_ERROR_MAX_SESSIONS_EXCEEDED,
+                STATUS_CODE_ERROR_SESSION_NOT_CONFIGURED,
+                STATUS_CODE_ERROR_ACTIVE_SESSIONS_ONGOING,
+                STATUS_CODE_ERROR_MULTICAST_LIST_FULL,
+                STATUS_CODE_ERROR_ADDRESS_NOT_FOUND,
+                STATUS_CODE_ERROR_ADDRESS_ALREADY_PRESENT,
+                STATUS_CODE_RANGING_TX_FAILED,
+                STATUS_CODE_RANGING_RX_TIMEOUT,
+                STATUS_CODE_RANGING_RX_PHY_DEC_FAILED,
+                STATUS_CODE_RANGING_RX_PHY_TOA_FAILED,
+                STATUS_CODE_RANGING_RX_PHY_STS_FAILED,
+                STATUS_CODE_RANGING_RX_MAC_DEC_FAILED,
+                STATUS_CODE_RANGING_RX_MAC_IE_DEC_FAILED,
+                STATUS_CODE_RANGING_RX_MAC_IE_MISSING,
+            })
+    public @interface StatusCode {}
+
+    public static final int STATUS_CODE_OK = 0x00;
+    public static final int STATUS_CODE_REJECTED = 0x01;
+    public static final int STATUS_CODE_FAILED = 0x02;
+    public static final int STATUS_CODE_SYNTAX_ERROR = 0x03;
+    public static final int STATUS_CODE_INVALID_PARAM = 0x04;
+    public static final int STATUS_CODE_INVALID_RANGE = 0x05;
+    public static final int STATUS_CODE_INVALID_MESSAGE_SIZE = 0x06;
+    public static final int STATUS_CODE_UNKNOWN_GID = 0x07;
+    public static final int STATUS_CODE_UNKNOWN_OID = 0x08;
+    public static final int STATUS_CODE_READ_ONLY = 0x09;
+    public static final int STATUS_CODE_COMMAND_RETRY = 0x0A;
+    public static final int STATUS_CODE_ERROR_SESSION_NOT_EXIST = 0x11;
+    public static final int STATUS_CODE_ERROR_SESSION_DUPLICATE = 0x12;
+    public static final int STATUS_CODE_ERROR_SESSION_ACTIVE = 0x13;
+    public static final int STATUS_CODE_ERROR_MAX_SESSIONS_EXCEEDED = 0x14;
+    public static final int STATUS_CODE_ERROR_SESSION_NOT_CONFIGURED = 0x15;
+    public static final int STATUS_CODE_ERROR_ACTIVE_SESSIONS_ONGOING = 0x16;
+    public static final int STATUS_CODE_ERROR_MULTICAST_LIST_FULL = 0x17;
+    public static final int STATUS_CODE_ERROR_ADDRESS_NOT_FOUND = 0x18;
+    public static final int STATUS_CODE_ERROR_ADDRESS_ALREADY_PRESENT = 0x19;
+    public static final int STATUS_CODE_RANGING_TX_FAILED = 0x20;
+    public static final int STATUS_CODE_RANGING_RX_TIMEOUT = 0x21;
+    public static final int STATUS_CODE_RANGING_RX_PHY_DEC_FAILED = 0x22;
+    public static final int STATUS_CODE_RANGING_RX_PHY_TOA_FAILED = 0x23;
+    public static final int STATUS_CODE_RANGING_RX_PHY_STS_FAILED = 0x24;
+    public static final int STATUS_CODE_RANGING_RX_MAC_DEC_FAILED = 0x25;
+    public static final int STATUS_CODE_RANGING_RX_MAC_IE_DEC_FAILED = 0x26;
+    public static final int STATUS_CODE_RANGING_RX_MAC_IE_MISSING = 0x27;
+
+    /** State change reason codes defined in UCI table-15 */
+    @IntDef(
+            value = {
+                STATE_CHANGE_REASON_CODE_BY_COMMANDS,
+                STATE_CHANGE_REASON_CODE_MAX_RR_RETRY_REACHED,
+                STATE_CHANGE_REASON_CODE_ERROR_SLOT_LENGTH_NOT_SUPPORTED,
+                STATE_CHANGE_REASON_CODE_ERROR_INSUFFICIENT_SLOTS_PER_RR,
+                STATE_CHANGE_REASON_CODE_ERROR_MAC_ADDRESS_MODE_NOT_SUPPORTED,
+                STATE_CHANGE_REASON_CODE_ERROR_INVALID_RANGING_INTERVAL,
+                STATE_CHANGE_REASON_CODE_ERROR_INVALID_STS_CONFIG,
+                STATE_CHANGE_REASON_CODE_ERROR_INVALID_RFRAME_CONFIG,
+            })
+    public @interface StateChangeReasonCode {}
+
+    public static final int STATE_CHANGE_REASON_CODE_BY_COMMANDS = 0;
+    public static final int STATE_CHANGE_REASON_CODE_MAX_RR_RETRY_REACHED = 1;
+    public static final int STATE_CHANGE_REASON_CODE_ERROR_SLOT_LENGTH_NOT_SUPPORTED = 0x20;
+    public static final int STATE_CHANGE_REASON_CODE_ERROR_INSUFFICIENT_SLOTS_PER_RR = 0x21;
+    public static final int STATE_CHANGE_REASON_CODE_ERROR_MAC_ADDRESS_MODE_NOT_SUPPORTED = 0x22;
+    public static final int STATE_CHANGE_REASON_CODE_ERROR_INVALID_RANGING_INTERVAL = 0x23;
+    public static final int STATE_CHANGE_REASON_CODE_ERROR_INVALID_STS_CONFIG = 0x24;
+    public static final int STATE_CHANGE_REASON_CODE_ERROR_INVALID_RFRAME_CONFIG = 0x25;
+
+    /** Multicast controlee add/delete actions defined in UCI */
+    @IntDef(
+            value = {
+                MULTICAST_LIST_UPDATE_ACTION_ADD,
+                MULTICAST_LIST_UPDATE_ACTION_DELETE,
+            })
+    public @interface MulticastListUpdateAction {}
+
+    public static final int MULTICAST_LIST_UPDATE_ACTION_ADD = 0;
+    public static final int MULTICAST_LIST_UPDATE_ACTION_DELETE = 1;
+
+    @IntDef(
+            value = {
+                MULTICAST_LIST_UPDATE_STATUS_OK,
+                MULTICAST_LIST_UPDATE_STATUS_ERROR_MULTICAST_LIST_FULL,
+                MULTICAST_LIST_UPDATE_STATUS_ERROR_KEY_FETCH_FAIL,
+                MULTICAST_LIST_UPDATE_STATUS_ERROR_SUB_SESSION_ID_NOT_FOUND,
+            })
+    public @interface MulticastListUpdateStatus {}
+
+    public static final int MULTICAST_LIST_UPDATE_STATUS_OK = 0;
+    public static final int MULTICAST_LIST_UPDATE_STATUS_ERROR_MULTICAST_LIST_FULL = 1;
+    public static final int MULTICAST_LIST_UPDATE_STATUS_ERROR_KEY_FETCH_FAIL = 2;
+    public static final int MULTICAST_LIST_UPDATE_STATUS_ERROR_SUB_SESSION_ID_NOT_FOUND = 3;
+
+    /** Capability related definitions starts from here */
+    @IntDef(
+            value = {
+                DEVICE_CLASS_1,
+                DEVICE_CLASS_2,
+                DEVICE_CLASS_3,
+            })
+    public @interface DeviceClass {}
+
+    public static final int DEVICE_CLASS_1 = 1; // Controller & controlee
+    public static final int DEVICE_CLASS_2 = 2; // Controller
+    public static final int DEVICE_CLASS_3 = 3; // Controlee
+
+    public enum AoaCapabilityFlag implements FlagEnum {
+        HAS_AZIMUTH_SUPPORT(1),
+        HAS_ELEVATION_SUPPORT(1 << 1),
+        HAS_FOM_SUPPORT(1 << 2),
+        HAS_FULL_AZIMUTH_SUPPORT(1 << 3),
+        HAS_INTERLEAVING_SUPPORT(1 << 4);
+
+        private final long mValue;
+
+        private AoaCapabilityFlag(long value) {
+            mValue = value;
+        }
+
+        @Override
+        public long getValue() {
+            return mValue;
+        }
+    }
+
+    public enum DeviceRoleCapabilityFlag implements FlagEnum {
+        HAS_CONTROLEE_INITIATOR_SUPPORT(1),
+        HAS_CONTROLEE_RESPONDER_SUPPORT(1 << 1),
+        HAS_CONTROLLER_INITIATOR_SUPPORT(1 << 2),
+        HAS_CONTROLLER_RESPONDER_SUPPORT(1 << 3);
+
+        private final long mValue;
+
+        private DeviceRoleCapabilityFlag(long value) {
+            mValue = value;
+        }
+
+        @Override
+        public long getValue() {
+            return mValue;
+        }
+    }
+
+    public enum MultiNodeCapabilityFlag implements FlagEnum {
+        HAS_UNICAST_SUPPORT(1),
+        HAS_ONE_TO_MANY_SUPPORT(1 << 1),
+        HAS_MANY_TO_MANY_SUPPORT(1 << 2);
+
+        private final long mValue;
+
+        private MultiNodeCapabilityFlag(long value) {
+            mValue = value;
+        }
+
+        @Override
+        public long getValue() {
+            return mValue;
+        }
+    }
+
+    public enum PrfCapabilityFlag implements FlagEnum {
+        HAS_BPRF_SUPPORT(1),
+        HAS_HPRF_SUPPORT(1 << 1);
+
+        private final long mValue;
+
+        private PrfCapabilityFlag(long value) {
+            mValue = value;
+        }
+
+        @Override
+        public long getValue() {
+            return mValue;
+        }
+    }
+
+    public enum RangingRoundCapabilityFlag implements FlagEnum {
+        HAS_DS_TWR_SUPPORT(1),
+        HAS_SS_TWR_SUPPORT(1 << 1);
+
+        private final long mValue;
+
+        private RangingRoundCapabilityFlag(long value) {
+            mValue = value;
+        }
+
+        @Override
+        public long getValue() {
+            return mValue;
+        }
+    }
+
+    public enum RframeCapabilityFlag implements FlagEnum {
+        HAS_SP0_RFRAME_SUPPORT(1),
+        HAS_SP1_RFRAME_SUPPORT(1 << 1),
+        HAS_SP3_RFRAME_SUPPORT(1 << 3);
+
+        private final long mValue;
+
+        private RframeCapabilityFlag(long value) {
+            mValue = value;
+        }
+
+        @Override
+        public long getValue() {
+            return mValue;
+        }
+    }
+
+    public enum StsCapabilityFlag implements FlagEnum {
+        HAS_STATIC_STS_SUPPORT(1),
+        HAS_DYNAMIC_STS_SUPPORT(1 << 1),
+        HAS_DYNAMIC_STS_INDIVIDUAL_CONTROLEE_KEY_SUPPORT(1 << 2);
+
+        private final long mValue;
+
+        private StsCapabilityFlag(long value) {
+            mValue = value;
+        }
+
+        @Override
+        public long getValue() {
+            return mValue;
+        }
+    }
+
+    public enum PsduDataRateCapabilityFlag implements FlagEnum {
+        HAS_6M81_SUPPORT(1),
+        HAS_7M80_SUPPORT(1 << 1),
+        HAS_27M2_SUPPORT(1 << 2),
+        HAS_31M2_SUPPORT(1 << 3);
+
+        private final long mValue;
+
+        private PsduDataRateCapabilityFlag(long value) {
+            mValue = value;
+        }
+
+        @Override
+        public long getValue() {
+            return mValue;
+        }
+    }
+
+    public enum BprfParameterSetCapabilityFlag implements FlagEnum {
+        HAS_SET_1_SUPPORT(1),
+        HAS_SET_2_SUPPORT(1 << 1),
+        HAS_SET_3_SUPPORT(1 << 2),
+        HAS_SET_4_SUPPORT(1 << 3),
+        HAS_SET_5_SUPPORT(1 << 4),
+        HAS_SET_6_SUPPORT(1 << 5);
+
+        private final long mValue;
+
+        private BprfParameterSetCapabilityFlag(long value) {
+            mValue = value;
+        }
+
+        @Override
+        public long getValue() {
+            return mValue;
+        }
+    }
+
+    public enum HprfParameterSetCapabilityFlag implements FlagEnum {
+        HAS_SET_1_SUPPORT(1L),
+        HAS_SET_2_SUPPORT(1L << 1),
+        HAS_SET_3_SUPPORT(1L << 2),
+        HAS_SET_4_SUPPORT(1L << 3),
+        HAS_SET_5_SUPPORT(1L << 4),
+        HAS_SET_6_SUPPORT(1L << 5),
+        HAS_SET_7_SUPPORT(1L << 6),
+        HAS_SET_8_SUPPORT(1L << 7),
+        HAS_SET_9_SUPPORT(1L << 8),
+        HAS_SET_10_SUPPORT(1L << 9),
+        HAS_SET_11_SUPPORT(1L << 10),
+        HAS_SET_12_SUPPORT(1L << 11),
+        HAS_SET_13_SUPPORT(1L << 12),
+        HAS_SET_14_SUPPORT(1L << 13),
+        HAS_SET_15_SUPPORT(1L << 14),
+        HAS_SET_16_SUPPORT(1L << 15),
+        HAS_SET_17_SUPPORT(1L << 16),
+        HAS_SET_18_SUPPORT(1L << 17),
+        HAS_SET_19_SUPPORT(1L << 18),
+        HAS_SET_20_SUPPORT(1L << 19),
+        HAS_SET_21_SUPPORT(1L << 20),
+        HAS_SET_22_SUPPORT(1L << 21),
+        HAS_SET_23_SUPPORT(1L << 22),
+        HAS_SET_24_SUPPORT(1L << 23),
+        HAS_SET_25_SUPPORT(1L << 24),
+        HAS_SET_26_SUPPORT(1L << 25),
+        HAS_SET_27_SUPPORT(1L << 26),
+        HAS_SET_28_SUPPORT(1L << 27),
+        HAS_SET_29_SUPPORT(1L << 28),
+        HAS_SET_30_SUPPORT(1L << 29),
+        HAS_SET_31_SUPPORT(1L << 30),
+        HAS_SET_32_SUPPORT(1L << 31),
+        HAS_SET_33_SUPPORT(1L << 32),
+        HAS_SET_34_SUPPORT(1L << 33),
+        HAS_SET_35_SUPPORT(1L << 34);
+
+        private final long mValue;
+
+        private HprfParameterSetCapabilityFlag(long value) {
+            mValue = value;
+        }
+
+        @Override
+        public long getValue() {
+            return mValue;
+        }
+    }
+
+    // Helper functions
+    protected static UwbAddress longToUwbAddress(long value, int length) {
+        ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
+        buffer.putLong(value);
+        return UwbAddress.fromBytes(Arrays.copyOf(buffer.array(), length));
+    }
+
+    protected static long uwbAddressToLong(UwbAddress address) {
+        ByteBuffer buffer = ByteBuffer.wrap(Arrays.copyOf(address.toBytes(), Long.BYTES));
+        return buffer.getLong();
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/fira/FiraProtocolVersion.java b/service/support_lib/src/com/google/uwb/support/fira/FiraProtocolVersion.java
new file mode 100644
index 0000000..50a3808
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/fira/FiraProtocolVersion.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.fira;
+
+import com.google.uwb.support.base.ProtocolVersion;
+
+import java.nio.ByteBuffer;
+
+/** Provides parameter versioning for Fira. */
+public class FiraProtocolVersion extends ProtocolVersion {
+    private static final int FIRA_PACKED_BYTE_COUNT = 2;
+
+    public FiraProtocolVersion(int major, int minor) {
+        super(major, minor);
+    }
+
+    public static FiraProtocolVersion fromString(String protocol) {
+        String[] parts = protocol.split("\\.", -1);
+        if (parts.length != 2) {
+            throw new IllegalArgumentException("Invalid protocol version: " + protocol);
+        }
+
+        return new FiraProtocolVersion(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
+    }
+
+    public byte[] toBytes() {
+        return ByteBuffer.allocate(bytesUsed())
+                .put((byte) getMajor())
+                .put((byte) getMinor())
+                .array();
+    }
+
+    public static FiraProtocolVersion fromBytes(byte[] data, int startIndex) {
+        int major = data[startIndex];
+        int minor = data[startIndex + 1];
+        return new FiraProtocolVersion(major, minor);
+    }
+
+    public static int bytesUsed() {
+        return FIRA_PACKED_BYTE_COUNT;
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/fira/FiraRangingReconfigureParams.java b/service/support_lib/src/com/google/uwb/support/fira/FiraRangingReconfigureParams.java
new file mode 100644
index 0000000..a203dde
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/fira/FiraRangingReconfigureParams.java
@@ -0,0 +1,299 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.fira;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import static java.util.Objects.requireNonNull;
+
+import android.os.PersistableBundle;
+import android.uwb.RangingSession;
+import android.uwb.UwbAddress;
+
+import androidx.annotation.Nullable;
+
+/**
+ * UWB parameters used to reconfigure a FiRa session. Supports peer adding/removing.
+ *
+ * <p>This is passed as a bundle to the service API {@link RangingSession#reconfigure}.
+ */
+public class FiraRangingReconfigureParams extends FiraParams {
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    @Nullable @MulticastListUpdateAction private final Integer mAction;
+    @Nullable private final UwbAddress[] mAddressList;
+    @Nullable private final int[] mSubSessionIdList;
+
+    @Nullable private final Integer mBlockStrideLength;
+
+    @Nullable @RangeDataNtfConfig private final Integer mRangeDataNtfConfig;
+    @Nullable private final Integer mRangeDataProximityNear;
+    @Nullable private final Integer mRangeDataProximityFar;
+
+    private static final String KEY_ACTION = "action";
+    private static final String KEY_MAC_ADDRESS_MODE = "mac_address_mode";
+    private static final String KEY_ADDRESS_LIST = "address_list";
+    private static final String KEY_SUB_SESSION_ID_LIST = "sub_session_id_list";
+    private static final String KEY_UPDATE_BLOCK_STRIDE_LENGTH = "update_block_stride_length";
+    private static final String KEY_UPDATE_RANGE_DATA_NTF_CONFIG = "update_range_data_ntf_config";
+    private static final String KEY_UPDATE_RANGE_DATA_NTF_PROXIMITY_NEAR =
+            "update_range_data_proximity_near";
+    private static final String KEY_UPDATE_RANGE_DATA_NTF_PROXIMITY_FAR =
+            "update_range_data_proximity_far";
+
+    private FiraRangingReconfigureParams(
+            @Nullable @MulticastListUpdateAction Integer action,
+            @Nullable UwbAddress[] addressList,
+            @Nullable int[] subSessionIdList,
+            @Nullable Integer blockStrideLength,
+            @Nullable Integer rangeDataNtfConfig,
+            @Nullable Integer rangeDataProximityNear,
+            @Nullable Integer rangeDataProximityFar) {
+        mAction = action;
+        mAddressList = addressList;
+        mSubSessionIdList = subSessionIdList;
+        mBlockStrideLength = blockStrideLength;
+        mRangeDataNtfConfig = rangeDataNtfConfig;
+        mRangeDataProximityNear = rangeDataProximityNear;
+        mRangeDataProximityFar = rangeDataProximityFar;
+    }
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    @Nullable
+    @MulticastListUpdateAction
+    public Integer getAction() {
+        return mAction;
+    }
+
+    @Nullable
+    public UwbAddress[] getAddressList() {
+        return mAddressList;
+    }
+
+    @Nullable
+    public int[] getSubSessionIdList() {
+        return mSubSessionIdList;
+    }
+
+    @Nullable
+    public Integer getBlockStrideLength() {
+        return mBlockStrideLength;
+    }
+
+    @Nullable
+    public Integer getRangeDataNtfConfig() {
+        return mRangeDataNtfConfig;
+    }
+
+    @Nullable
+    public Integer getRangeDataProximityNear() {
+        return mRangeDataProximityNear;
+    }
+
+    @Nullable
+    public Integer getRangeDataProximityFar() {
+        return mRangeDataProximityFar;
+    }
+
+    @Override
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = super.toBundle();
+        if (mAction != null) {
+            requireNonNull(mAddressList);
+            bundle.putInt(KEY_ACTION, mAction);
+
+            long[] addressList = new long[mAddressList.length];
+            int i = 0;
+            for (UwbAddress address : mAddressList) {
+                addressList[i++] = uwbAddressToLong(address);
+            }
+            int macAddressMode = MAC_ADDRESS_MODE_2_BYTES;
+            if (mAddressList[0].size() == UwbAddress.EXTENDED_ADDRESS_BYTE_LENGTH) {
+                macAddressMode = MAC_ADDRESS_MODE_8_BYTES;
+            }
+            bundle.putInt(KEY_MAC_ADDRESS_MODE, macAddressMode);
+            bundle.putLongArray(KEY_ADDRESS_LIST, addressList);
+            bundle.putIntArray(KEY_SUB_SESSION_ID_LIST, mSubSessionIdList);
+        }
+
+        if (mBlockStrideLength != null) {
+            bundle.putInt(KEY_UPDATE_BLOCK_STRIDE_LENGTH, mBlockStrideLength);
+        }
+
+        if (mRangeDataNtfConfig != null) {
+            bundle.putInt(KEY_UPDATE_RANGE_DATA_NTF_CONFIG, mRangeDataNtfConfig);
+        }
+
+        if (mRangeDataProximityNear != null) {
+            bundle.putInt(KEY_UPDATE_RANGE_DATA_NTF_PROXIMITY_NEAR, mRangeDataProximityNear);
+        }
+
+        if (mRangeDataProximityFar != null) {
+            bundle.putInt(KEY_UPDATE_RANGE_DATA_NTF_PROXIMITY_FAR, mRangeDataProximityFar);
+        }
+
+        return bundle;
+    }
+
+    public static FiraRangingReconfigureParams fromBundle(PersistableBundle bundle) {
+        if (!isCorrectProtocol(bundle)) {
+            throw new IllegalArgumentException("Invalid protocol");
+        }
+
+        switch (getBundleVersion(bundle)) {
+            case BUNDLE_VERSION_1:
+                return parseVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("Invalid bundle version");
+        }
+    }
+
+    private static FiraRangingReconfigureParams parseVersion1(PersistableBundle bundle) {
+        FiraRangingReconfigureParams.Builder builder = new FiraRangingReconfigureParams.Builder();
+        if (bundle.containsKey(KEY_ACTION)) {
+            int macAddressMode = bundle.getInt(KEY_MAC_ADDRESS_MODE);
+            int addressByteLength = UwbAddress.SHORT_ADDRESS_BYTE_LENGTH;
+            if (macAddressMode == MAC_ADDRESS_MODE_8_BYTES) {
+                addressByteLength = UwbAddress.EXTENDED_ADDRESS_BYTE_LENGTH;
+            }
+
+            long[] addresses = bundle.getLongArray(KEY_ADDRESS_LIST);
+            UwbAddress[] addressList = new UwbAddress[addresses.length];
+            for (int i = 0; i < addresses.length; i++) {
+                addressList[i] = longToUwbAddress(addresses[i], addressByteLength);
+            }
+            builder.setAction(bundle.getInt(KEY_ACTION))
+                    .setAddressList(addressList)
+                    .setSubSessionIdList(bundle.getIntArray(KEY_SUB_SESSION_ID_LIST));
+        }
+
+        if (bundle.containsKey(KEY_UPDATE_BLOCK_STRIDE_LENGTH)) {
+            builder.setBlockStrideLength(bundle.getInt(KEY_UPDATE_BLOCK_STRIDE_LENGTH));
+        }
+
+        if (bundle.containsKey(KEY_UPDATE_RANGE_DATA_NTF_CONFIG)) {
+            builder.setRangeDataNtfConfig(bundle.getInt(KEY_UPDATE_RANGE_DATA_NTF_CONFIG));
+        }
+
+        if (bundle.containsKey(KEY_UPDATE_RANGE_DATA_NTF_PROXIMITY_NEAR)) {
+            builder.setRangeDataProximityNear(
+                    bundle.getInt(KEY_UPDATE_RANGE_DATA_NTF_PROXIMITY_NEAR));
+        }
+
+        if (bundle.containsKey(KEY_UPDATE_RANGE_DATA_NTF_PROXIMITY_FAR)) {
+            builder.setRangeDataProximityFar(
+                    bundle.getInt(KEY_UPDATE_RANGE_DATA_NTF_PROXIMITY_FAR));
+        }
+
+        return builder.build();
+    }
+
+    /** Builder */
+    public static class Builder {
+        @Nullable private Integer mAction = null;
+        @Nullable private UwbAddress[] mAddressList = null;
+        @Nullable private int[] mSubSessionIdList = null;
+
+        @Nullable private Integer mBlockStrideLength = null;
+
+        @Nullable private Integer mRangeDataNtfConfig = null;
+        @Nullable private Integer mRangeDataProximityNear = null;
+        @Nullable private Integer mRangeDataProximityFar = null;
+
+        public FiraRangingReconfigureParams.Builder setAction(
+                @MulticastListUpdateAction int action) {
+            mAction = action;
+            return this;
+        }
+
+        public FiraRangingReconfigureParams.Builder setAddressList(UwbAddress[] addressList) {
+            mAddressList = addressList;
+            return this;
+        }
+
+        public FiraRangingReconfigureParams.Builder setSubSessionIdList(int[] subSessionIdList) {
+            mSubSessionIdList = subSessionIdList;
+            return this;
+        }
+
+        public FiraRangingReconfigureParams.Builder setBlockStrideLength(int blockStrideLength) {
+            mBlockStrideLength = blockStrideLength;
+            return this;
+        }
+
+        public FiraRangingReconfigureParams.Builder setRangeDataNtfConfig(int rangeDataNtfConfig) {
+            mRangeDataNtfConfig = rangeDataNtfConfig;
+            return this;
+        }
+
+        public FiraRangingReconfigureParams.Builder setRangeDataProximityNear(
+                int rangeDataProximityNear) {
+            mRangeDataProximityNear = rangeDataProximityNear;
+            return this;
+        }
+
+        public FiraRangingReconfigureParams.Builder setRangeDataProximityFar(
+                int rangeDataProximityFar) {
+            mRangeDataProximityFar = rangeDataProximityFar;
+            return this;
+        }
+
+        private void checkAddressList() {
+            checkArgument(mAddressList != null && mAddressList.length > 0);
+            for (UwbAddress uwbAddress : mAddressList) {
+                requireNonNull(uwbAddress);
+                checkArgument(uwbAddress.size() == UwbAddress.SHORT_ADDRESS_BYTE_LENGTH);
+            }
+
+            checkArgument(
+                    mSubSessionIdList == null || mSubSessionIdList.length == mAddressList.length);
+        }
+
+        public FiraRangingReconfigureParams build() {
+            if (mAction != null) {
+                checkAddressList();
+                // Either update the address list or update ranging parameters. Not both.
+                checkArgument(
+                        mBlockStrideLength == null
+                                && mRangeDataNtfConfig == null
+                                && mRangeDataProximityNear == null
+                                && mRangeDataProximityFar == null);
+            } else {
+                checkArgument(
+                        mBlockStrideLength != null
+                                || mRangeDataNtfConfig != null
+                                || mRangeDataProximityNear != null
+                                || mRangeDataProximityFar != null);
+            }
+
+            return new FiraRangingReconfigureParams(
+                    mAction,
+                    mAddressList,
+                    mSubSessionIdList,
+                    mBlockStrideLength,
+                    mRangeDataNtfConfig,
+                    mRangeDataProximityNear,
+                    mRangeDataProximityFar);
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/fira/FiraSpecificationParams.java b/service/support_lib/src/com/google/uwb/support/fira/FiraSpecificationParams.java
new file mode 100644
index 0000000..acff26c
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/fira/FiraSpecificationParams.java
@@ -0,0 +1,509 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.fira;
+
+import static java.util.Objects.requireNonNull;
+
+import android.os.PersistableBundle;
+import android.uwb.UwbManager;
+
+import com.google.uwb.support.base.FlagEnum;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+
+/**
+ * Defines parameters for FIRA capability.
+ *
+ * <p>This is returned as a bundle from the service API {@link UwbManager#getSpecificationInfo}.
+ */
+public class FiraSpecificationParams extends FiraParams {
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    private final FiraProtocolVersion mMinPhyVersionSupported;
+    private final FiraProtocolVersion mMaxPhyVersionSupported;
+    private final FiraProtocolVersion mMinMacVersionSupported;
+    private final FiraProtocolVersion mMaxMacVersionSupported;
+
+    private final List<Integer> mSupportedChannels;
+
+    private final EnumSet<AoaCapabilityFlag> mAoaCapabilities;
+
+    private final EnumSet<DeviceRoleCapabilityFlag> mDeviceRoleCapabilities;
+
+    private final boolean mHasBlockStridingSupport;
+
+    private final boolean mHasNonDeferredModeSupport;
+
+    private final boolean mHasInitiationTimeSupport;
+
+    private final EnumSet<MultiNodeCapabilityFlag> mMultiNodeCapabilities;
+
+    private final EnumSet<PrfCapabilityFlag> mPrfCapabilities;
+
+    private final EnumSet<RangingRoundCapabilityFlag> mRangingRoundCapabilities;
+
+    private final EnumSet<RframeCapabilityFlag> mRframeCapabilities;
+
+    private final EnumSet<StsCapabilityFlag> mStsCapabilities;
+
+    private final EnumSet<PsduDataRateCapabilityFlag> mPsduDataRateCapabilities;
+
+    private final EnumSet<BprfParameterSetCapabilityFlag> mBprfParameterSetCapabilities;
+
+    private final EnumSet<HprfParameterSetCapabilityFlag> mHprfParameterSetCapabilities;
+
+    private static final String KEY_MIN_PHY_VERSION = "min_phy_version";
+    private static final String KEY_MAX_PHY_VERSION = "max_phy_version";
+    private static final String KEY_MIN_MAC_VERSION = "min_mac_version";
+    private static final String KEY_MAX_MAC_VERSION = "max_mac_version";
+
+    private static final String KEY_SUPPORTED_CHANNELS = "channels";
+    private static final String KEY_AOA_CAPABILITIES = "aoa_capabilities";
+    private static final String KEY_DEVICE_ROLE_CAPABILITIES = "device_role_capabilities";
+    private static final String KEY_BLOCK_STRIDING_SUPPORT = "block_striding";
+    private static final String KEY_NON_DEFERRED_MODE_SUPPORT = "non_deferred_mode";
+    private static final String KEY_INITIATION_TIME_SUPPORT = "initiation_time";
+    private static final String KEY_MULTI_NODE_CAPABILITIES = "multi_node_capabilities";
+    private static final String KEY_PRF_CAPABILITIES = "prf_capabilities";
+    private static final String KEY_RANGING_ROUND_CAPABILITIES = "ranging_round_capabilities";
+    private static final String KEY_RFRAME_CAPABILITIES = "rframe_capabilities";
+    private static final String KEY_STS_CAPABILITIES = "sts_capabilities";
+    private static final String KEY_PSDU_DATA_RATE_CAPABILITIES = "psdu_data_rate_capabilities";
+    private static final String KEY_BPRF_PARAMETER_SET_CAPABILITIES =
+            "bprf_parameter_set_capabilities";
+    private static final String KEY_HPRF_PARAMETER_SET_CAPABILITIES =
+            "hprf_parameter_set_capabilities";
+
+    private FiraSpecificationParams(
+            FiraProtocolVersion minPhyVersionSupported,
+            FiraProtocolVersion maxPhyVersionSupported,
+            FiraProtocolVersion minMacVersionSupported,
+            FiraProtocolVersion maxMacVersionSupported,
+            List<Integer> supportedChannels,
+            EnumSet<AoaCapabilityFlag> aoaCapabilities,
+            EnumSet<DeviceRoleCapabilityFlag> deviceRoleCapabilities,
+            boolean hasBlockStridingSupport,
+            boolean hasNonDeferredModeSupport,
+            boolean hasInitiationTimeSupport,
+            EnumSet<MultiNodeCapabilityFlag> multiNodeCapabilities,
+            EnumSet<PrfCapabilityFlag> prfCapabilities,
+            EnumSet<RangingRoundCapabilityFlag> rangingRoundCapabilities,
+            EnumSet<RframeCapabilityFlag> rframeCapabilities,
+            EnumSet<StsCapabilityFlag> stsCapabilities,
+            EnumSet<PsduDataRateCapabilityFlag> psduDataRateCapabilities,
+            EnumSet<BprfParameterSetCapabilityFlag> bprfParameterSetCapabilities,
+            EnumSet<HprfParameterSetCapabilityFlag> hprfParameterSetCapabilities) {
+        mMinPhyVersionSupported = minPhyVersionSupported;
+        mMaxPhyVersionSupported = maxPhyVersionSupported;
+        mMinMacVersionSupported = minMacVersionSupported;
+        mMaxMacVersionSupported = maxMacVersionSupported;
+        mSupportedChannels = supportedChannels;
+        mAoaCapabilities = aoaCapabilities;
+        mDeviceRoleCapabilities = deviceRoleCapabilities;
+        mHasBlockStridingSupport = hasBlockStridingSupport;
+        mHasNonDeferredModeSupport = hasNonDeferredModeSupport;
+        mHasInitiationTimeSupport = hasInitiationTimeSupport;
+        mMultiNodeCapabilities = multiNodeCapabilities;
+        mPrfCapabilities = prfCapabilities;
+        mRangingRoundCapabilities = rangingRoundCapabilities;
+        mRframeCapabilities = rframeCapabilities;
+        mStsCapabilities = stsCapabilities;
+        mPsduDataRateCapabilities = psduDataRateCapabilities;
+        mBprfParameterSetCapabilities = bprfParameterSetCapabilities;
+        mHprfParameterSetCapabilities = hprfParameterSetCapabilities;
+    }
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    public FiraProtocolVersion getMinPhyVersionSupported() {
+        return mMinPhyVersionSupported;
+    }
+
+    public FiraProtocolVersion getMaxPhyVersionSupported() {
+        return mMaxPhyVersionSupported;
+    }
+
+    public FiraProtocolVersion getMinMacVersionSupported() {
+        return mMinMacVersionSupported;
+    }
+
+    public FiraProtocolVersion getMaxMacVersionSupported() {
+        return mMaxMacVersionSupported;
+    }
+
+    public List<Integer> getSupportedChannels() {
+        return mSupportedChannels;
+    }
+
+    public EnumSet<AoaCapabilityFlag> getAoaCapabilities() {
+        return mAoaCapabilities;
+    }
+
+    public EnumSet<DeviceRoleCapabilityFlag> getDeviceRoleCapabilities() {
+        return mDeviceRoleCapabilities;
+    }
+
+    public boolean hasBlockStridingSupport() {
+        return mHasBlockStridingSupport;
+    }
+
+    public boolean hasNonDeferredModeSupport() {
+        return mHasNonDeferredModeSupport;
+    }
+
+    public boolean hasInitiationTimeSupport() {
+        return mHasInitiationTimeSupport;
+    }
+
+    public EnumSet<MultiNodeCapabilityFlag> getMultiNodeCapabilities() {
+        return mMultiNodeCapabilities;
+    }
+
+    public EnumSet<PrfCapabilityFlag> getPrfCapabilities() {
+        return mPrfCapabilities;
+    }
+
+    public EnumSet<RangingRoundCapabilityFlag> getRangingRoundCapabilities() {
+        return mRangingRoundCapabilities;
+    }
+
+    public EnumSet<RframeCapabilityFlag> getRframeCapabilities() {
+        return mRframeCapabilities;
+    }
+
+    public EnumSet<StsCapabilityFlag> getStsCapabilities() {
+        return mStsCapabilities;
+    }
+
+    public EnumSet<PsduDataRateCapabilityFlag> getPsduDataRateCapabilities() {
+        return mPsduDataRateCapabilities;
+    }
+
+    public EnumSet<BprfParameterSetCapabilityFlag> getBprfParameterSetCapabilities() {
+        return mBprfParameterSetCapabilities;
+    }
+
+    public EnumSet<HprfParameterSetCapabilityFlag> getHprfParameterSetCapabilities() {
+        return mHprfParameterSetCapabilities;
+    }
+
+    private static int[] toIntArray(List<Integer> data) {
+        int[] res = new int[data.size()];
+        for (int i = 0; i < data.size(); i++) {
+            res[i] = data.get(i);
+        }
+        return res;
+    }
+
+    @Override
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = super.toBundle();
+        bundle.putString(KEY_MIN_PHY_VERSION, mMinPhyVersionSupported.toString());
+        bundle.putString(KEY_MAX_PHY_VERSION, mMaxPhyVersionSupported.toString());
+        bundle.putString(KEY_MIN_MAC_VERSION, mMinMacVersionSupported.toString());
+        bundle.putString(KEY_MAX_MAC_VERSION, mMaxMacVersionSupported.toString());
+        bundle.putIntArray(KEY_SUPPORTED_CHANNELS, toIntArray(mSupportedChannels));
+        bundle.putInt(KEY_AOA_CAPABILITIES, FlagEnum.toInt(mAoaCapabilities));
+        bundle.putInt(KEY_DEVICE_ROLE_CAPABILITIES, FlagEnum.toInt(mDeviceRoleCapabilities));
+        bundle.putBoolean(KEY_BLOCK_STRIDING_SUPPORT, mHasBlockStridingSupport);
+        bundle.putBoolean(KEY_NON_DEFERRED_MODE_SUPPORT, mHasNonDeferredModeSupport);
+        bundle.putBoolean(KEY_INITIATION_TIME_SUPPORT, mHasInitiationTimeSupport);
+        bundle.putInt(KEY_MULTI_NODE_CAPABILITIES, FlagEnum.toInt(mMultiNodeCapabilities));
+        bundle.putInt(KEY_PRF_CAPABILITIES, FlagEnum.toInt(mPrfCapabilities));
+        bundle.putInt(KEY_RANGING_ROUND_CAPABILITIES, FlagEnum.toInt(mRangingRoundCapabilities));
+        bundle.putInt(KEY_RFRAME_CAPABILITIES, FlagEnum.toInt(mRframeCapabilities));
+        bundle.putInt(KEY_STS_CAPABILITIES, FlagEnum.toInt(mStsCapabilities));
+        bundle.putInt(KEY_PSDU_DATA_RATE_CAPABILITIES, FlagEnum.toInt(mPsduDataRateCapabilities));
+        bundle.putInt(KEY_BPRF_PARAMETER_SET_CAPABILITIES,
+                FlagEnum.toInt(mBprfParameterSetCapabilities));
+        bundle.putLong(KEY_HPRF_PARAMETER_SET_CAPABILITIES,
+                FlagEnum.toLong(mHprfParameterSetCapabilities));
+        return bundle;
+    }
+
+    public static FiraSpecificationParams fromBundle(PersistableBundle bundle) {
+        if (!isCorrectProtocol(bundle)) {
+            throw new IllegalArgumentException("Invalid protocol");
+        }
+
+        switch (getBundleVersion(bundle)) {
+            case BUNDLE_VERSION_1:
+                return parseVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("Invalid bundle version");
+        }
+    }
+
+    private static List<Integer> toIntList(int[] data) {
+        List<Integer> res = new ArrayList<>();
+        for (int datum : data) {
+            res.add(datum);
+        }
+        return res;
+    }
+
+    private static FiraSpecificationParams parseVersion1(PersistableBundle bundle) {
+        FiraSpecificationParams.Builder builder = new FiraSpecificationParams.Builder();
+        List<Integer> supportedChannels =
+                toIntList(requireNonNull(bundle.getIntArray(KEY_SUPPORTED_CHANNELS)));
+        return builder.setMinPhyVersionSupported(
+                        FiraProtocolVersion.fromString(bundle.getString(KEY_MIN_PHY_VERSION)))
+                .setMaxPhyVersionSupported(
+                        FiraProtocolVersion.fromString(bundle.getString(KEY_MAX_PHY_VERSION)))
+                .setMinMacVersionSupported(
+                        FiraProtocolVersion.fromString(bundle.getString(KEY_MIN_MAC_VERSION)))
+                .setMaxMacVersionSupported(
+                        FiraProtocolVersion.fromString(bundle.getString(KEY_MAX_MAC_VERSION)))
+                .setSupportedChannels(supportedChannels)
+                .setAoaCapabilities(
+                        FlagEnum.toEnumSet(
+                                bundle.getInt(KEY_AOA_CAPABILITIES), AoaCapabilityFlag.values()))
+                .setDeviceRoleCapabilities(
+                        FlagEnum.toEnumSet(
+                                bundle.getInt(KEY_DEVICE_ROLE_CAPABILITIES),
+                                DeviceRoleCapabilityFlag.values()))
+                .hasBlockStridingSupport(bundle.getBoolean(KEY_BLOCK_STRIDING_SUPPORT))
+                .hasNonDeferredModeSupport(bundle.getBoolean(KEY_NON_DEFERRED_MODE_SUPPORT))
+                .hasInitiationTimeSupport(bundle.getBoolean(KEY_INITIATION_TIME_SUPPORT))
+                .setMultiNodeCapabilities(
+                        FlagEnum.toEnumSet(
+                                bundle.getInt(KEY_MULTI_NODE_CAPABILITIES),
+                                MultiNodeCapabilityFlag.values()))
+                .setPrfCapabilities(
+                        FlagEnum.toEnumSet(
+                                bundle.getInt(KEY_PRF_CAPABILITIES), PrfCapabilityFlag.values()))
+                .setRangingRoundCapabilities(
+                        FlagEnum.toEnumSet(
+                                bundle.getInt(KEY_RANGING_ROUND_CAPABILITIES),
+                                RangingRoundCapabilityFlag.values()))
+                .setRframeCapabilities(
+                        FlagEnum.toEnumSet(
+                                bundle.getInt(KEY_RFRAME_CAPABILITIES),
+                                RframeCapabilityFlag.values()))
+                .setStsCapabilities(
+                        FlagEnum.toEnumSet(
+                                bundle.getInt(KEY_STS_CAPABILITIES), StsCapabilityFlag.values()))
+                .setPsduDataRateCapabilities(
+                        FlagEnum.toEnumSet(
+                                bundle.getInt(KEY_PSDU_DATA_RATE_CAPABILITIES),
+                                PsduDataRateCapabilityFlag.values()))
+                .setBprfParameterSetCapabilities(
+                        FlagEnum.toEnumSet(
+                                bundle.getInt(KEY_BPRF_PARAMETER_SET_CAPABILITIES),
+                                BprfParameterSetCapabilityFlag.values()))
+                .setHprfParameterSetCapabilities(
+                        FlagEnum.longToEnumSet(
+                                bundle.getLong(KEY_HPRF_PARAMETER_SET_CAPABILITIES),
+                                HprfParameterSetCapabilityFlag.values()))
+                .build();
+    }
+
+    /** Builder */
+    public static class Builder {
+        // Set all default protocol version to FiRa 1.1
+        private FiraProtocolVersion mMinPhyVersionSupported = new FiraProtocolVersion(1, 1);
+        private FiraProtocolVersion mMaxPhyVersionSupported = new FiraProtocolVersion(1, 1);
+        private FiraProtocolVersion mMinMacVersionSupported = new FiraProtocolVersion(1, 1);
+        private FiraProtocolVersion mMaxMacVersionSupported = new FiraProtocolVersion(1, 1);
+
+        private List<Integer> mSupportedChannels;
+
+        private final EnumSet<AoaCapabilityFlag> mAoaCapabilities =
+                EnumSet.noneOf(AoaCapabilityFlag.class);
+
+        // Controller-intiator, Cotrolee-responder are mandatory.
+        private final EnumSet<DeviceRoleCapabilityFlag> mDeviceRoleCapabilities =
+                EnumSet.of(
+                        DeviceRoleCapabilityFlag.HAS_CONTROLLER_INITIATOR_SUPPORT,
+                        DeviceRoleCapabilityFlag.HAS_CONTROLEE_RESPONDER_SUPPORT);
+
+        private boolean mHasBlockStridingSupport = false;
+
+        private boolean mHasNonDeferredModeSupport = false;
+
+        private boolean mHasInitiationTimeSupport = false;
+
+        // Unicast support is mandatory
+        private final EnumSet<MultiNodeCapabilityFlag> mMultiNodeCapabilities =
+                EnumSet.of(MultiNodeCapabilityFlag.HAS_UNICAST_SUPPORT);
+
+        // BPRF mode is mandatory
+        private final EnumSet<PrfCapabilityFlag> mPrfCapabilities =
+                EnumSet.of(PrfCapabilityFlag.HAS_BPRF_SUPPORT);
+
+        // DS-TWR is mandatory
+        private final EnumSet<RangingRoundCapabilityFlag> mRangingRoundCapabilities =
+                EnumSet.of(RangingRoundCapabilityFlag.HAS_DS_TWR_SUPPORT);
+
+        // SP3 RFrame is mandatory
+        private final EnumSet<RframeCapabilityFlag> mRframeCapabilities =
+                EnumSet.of(RframeCapabilityFlag.HAS_SP3_RFRAME_SUPPORT);
+
+        // STATIC STS is mandatory
+        private final EnumSet<StsCapabilityFlag> mStsCapabilities =
+                EnumSet.of(StsCapabilityFlag.HAS_STATIC_STS_SUPPORT);
+
+        // 6.81Mb/s PSDU data rate is mandatory
+        private final EnumSet<PsduDataRateCapabilityFlag> mPsduDataRateCapabilities =
+                EnumSet.of(PsduDataRateCapabilityFlag.HAS_6M81_SUPPORT);
+
+        private final EnumSet<BprfParameterSetCapabilityFlag> mBprfParameterSetCapabilities =
+                EnumSet.noneOf(BprfParameterSetCapabilityFlag.class);
+
+        private final EnumSet<HprfParameterSetCapabilityFlag> mHprfParameterSetCapabilities =
+                EnumSet.noneOf(HprfParameterSetCapabilityFlag.class);
+
+        public FiraSpecificationParams.Builder setMinPhyVersionSupported(
+                FiraProtocolVersion version) {
+            mMinPhyVersionSupported = version;
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder setMaxPhyVersionSupported(
+                FiraProtocolVersion version) {
+            mMaxPhyVersionSupported = version;
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder setMinMacVersionSupported(
+                FiraProtocolVersion version) {
+            mMinMacVersionSupported = version;
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder setMaxMacVersionSupported(
+                FiraProtocolVersion version) {
+            mMaxMacVersionSupported = version;
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder setSupportedChannels(
+                List<Integer> supportedChannels) {
+            mSupportedChannels = List.copyOf(supportedChannels);
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder setAoaCapabilities(
+                Collection<AoaCapabilityFlag> aoaCapabilities) {
+            mAoaCapabilities.addAll(aoaCapabilities);
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder setDeviceRoleCapabilities(
+                Collection<DeviceRoleCapabilityFlag> deviceRoleCapabilities) {
+            mDeviceRoleCapabilities.addAll(deviceRoleCapabilities);
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder hasBlockStridingSupport(boolean value) {
+            mHasBlockStridingSupport = value;
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder hasNonDeferredModeSupport(boolean value) {
+            mHasNonDeferredModeSupport = value;
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder hasInitiationTimeSupport(boolean value) {
+            mHasInitiationTimeSupport = value;
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder setMultiNodeCapabilities(
+                Collection<MultiNodeCapabilityFlag> multiNodeCapabilities) {
+            mMultiNodeCapabilities.addAll(multiNodeCapabilities);
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder setPrfCapabilities(
+                Collection<PrfCapabilityFlag> prfCapabilities) {
+            mPrfCapabilities.addAll(prfCapabilities);
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder setRangingRoundCapabilities(
+                Collection<RangingRoundCapabilityFlag> rangingRoundCapabilities) {
+            mRangingRoundCapabilities.addAll(rangingRoundCapabilities);
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder setRframeCapabilities(
+                Collection<RframeCapabilityFlag> rframeCapabilities) {
+            mRframeCapabilities.addAll(rframeCapabilities);
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder setStsCapabilities(
+                Collection<StsCapabilityFlag> stsCapabilities) {
+            mStsCapabilities.addAll(stsCapabilities);
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder setPsduDataRateCapabilities(
+                Collection<PsduDataRateCapabilityFlag> psduDataRateCapabilities) {
+            mPsduDataRateCapabilities.addAll(psduDataRateCapabilities);
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder setBprfParameterSetCapabilities(
+                Collection<BprfParameterSetCapabilityFlag> bprfParameterSetCapabilities) {
+            mBprfParameterSetCapabilities.addAll(bprfParameterSetCapabilities);
+            return this;
+        }
+
+        public FiraSpecificationParams.Builder setHprfParameterSetCapabilities(
+                Collection<HprfParameterSetCapabilityFlag> hprfParameterSetCapabilities) {
+            mHprfParameterSetCapabilities.addAll(hprfParameterSetCapabilities);
+            return this;
+        }
+
+        public FiraSpecificationParams build() {
+            if (mSupportedChannels == null || mSupportedChannels.size() == 0) {
+                throw new IllegalStateException("Supported channels are not set");
+            }
+
+            return new FiraSpecificationParams(
+                    mMinPhyVersionSupported,
+                    mMaxPhyVersionSupported,
+                    mMinMacVersionSupported,
+                    mMaxMacVersionSupported,
+                    mSupportedChannels,
+                    mAoaCapabilities,
+                    mDeviceRoleCapabilities,
+                    mHasBlockStridingSupport,
+                    mHasNonDeferredModeSupport,
+                    mHasInitiationTimeSupport,
+                    mMultiNodeCapabilities,
+                    mPrfCapabilities,
+                    mRangingRoundCapabilities,
+                    mRframeCapabilities,
+                    mStsCapabilities,
+                    mPsduDataRateCapabilities,
+                    mBprfParameterSetCapabilities,
+                    mHprfParameterSetCapabilities);
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/fira/FiraStateChangeReasonCode.java b/service/support_lib/src/com/google/uwb/support/fira/FiraStateChangeReasonCode.java
new file mode 100644
index 0000000..0388b9c
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/fira/FiraStateChangeReasonCode.java
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.fira;
+
+import android.os.PersistableBundle;
+
+import com.google.uwb.support.base.RequiredParam;
+
+/** FiRa State Change code defined in UCI 1.2 Table 15 */
+public class FiraStateChangeReasonCode extends FiraParams {
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    @StateChangeReasonCode private final int mReasonCode;
+
+    private static final String KEY_REASON_CODE = "reason_code";
+
+    private FiraStateChangeReasonCode(@StateChangeReasonCode int reasonCode) {
+        mReasonCode = reasonCode;
+    }
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    @StateChangeReasonCode
+    public int getReasonCode() {
+        return mReasonCode;
+    }
+
+    @Override
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = super.toBundle();
+        bundle.putInt(KEY_REASON_CODE, mReasonCode);
+        return bundle;
+    }
+
+    public static FiraStateChangeReasonCode fromBundle(PersistableBundle bundle) {
+        if (!isCorrectProtocol(bundle)) {
+            throw new IllegalArgumentException("Invalid protocol");
+        }
+
+        switch (getBundleVersion(bundle)) {
+            case BUNDLE_VERSION_1:
+                return parseVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("Invalid bundle version");
+        }
+    }
+
+    private static FiraStateChangeReasonCode parseVersion1(PersistableBundle bundle) {
+        return new FiraStateChangeReasonCode.Builder()
+                .setReasonCode(bundle.getInt(KEY_REASON_CODE))
+                .build();
+    }
+
+    public static boolean isBundleValid(PersistableBundle bundle) {
+        return bundle.containsKey(KEY_REASON_CODE);
+    }
+
+    /** Builder */
+    public static class Builder {
+        private final RequiredParam<Integer> mReasonCode = new RequiredParam<>();
+
+        public FiraStateChangeReasonCode.Builder setReasonCode(int reasonCode) {
+            mReasonCode.set(reasonCode);
+            return this;
+        }
+
+        public FiraStateChangeReasonCode build() {
+            return new FiraStateChangeReasonCode(mReasonCode.get());
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/fira/FiraStatusCode.java b/service/support_lib/src/com/google/uwb/support/fira/FiraStatusCode.java
new file mode 100644
index 0000000..9848c79
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/fira/FiraStatusCode.java
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.fira;
+
+import android.os.PersistableBundle;
+
+import com.google.uwb.support.base.RequiredParam;
+
+/** FiRa status code defined in Table 32 */
+public class FiraStatusCode extends FiraParams {
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    @StatusCode private final int mStatusCode;
+
+    private static final String KEY_STATUS_CODE = "status_code";
+
+    private FiraStatusCode(@StatusCode int statusCode) {
+        mStatusCode = statusCode;
+    }
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    @StatusCode
+    public int getStatusCode() {
+        return mStatusCode;
+    }
+
+    @Override
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = super.toBundle();
+        bundle.putInt(KEY_STATUS_CODE, mStatusCode);
+        return bundle;
+    }
+
+    public static FiraStatusCode fromBundle(PersistableBundle bundle) {
+        if (!isCorrectProtocol(bundle)) {
+            throw new IllegalArgumentException("Invalid protocol");
+        }
+
+        switch (getBundleVersion(bundle)) {
+            case BUNDLE_VERSION_1:
+                return parseVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("Invalid bundle version");
+        }
+    }
+
+    private static FiraStatusCode parseVersion1(PersistableBundle bundle) {
+        return new FiraStatusCode.Builder().setStatusCode(bundle.getInt(KEY_STATUS_CODE)).build();
+    }
+
+    public static boolean isBundleValid(PersistableBundle bundle) {
+        return bundle.containsKey(KEY_STATUS_CODE);
+    }
+
+    /** Builder */
+    public static class Builder {
+        private final RequiredParam<Integer> mStatusCode = new RequiredParam<>();
+
+        public FiraStatusCode.Builder setStatusCode(int statusCode) {
+            mStatusCode.set(statusCode);
+            return this;
+        }
+
+        public FiraStatusCode build() {
+            return new FiraStatusCode(mStatusCode.get());
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/generic/GenericParams.java b/service/support_lib/src/com/google/uwb/support/generic/GenericParams.java
new file mode 100644
index 0000000..1a6e3ea
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/generic/GenericParams.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.generic;
+
+import android.os.Build.VERSION_CODES;
+import android.os.PersistableBundle;
+
+import androidx.annotation.RequiresApi;
+
+import com.google.uwb.support.base.Params;
+
+@RequiresApi(VERSION_CODES.LOLLIPOP)
+public abstract class GenericParams extends Params {
+    public static final String PROTOCOL_NAME = "generic";
+
+    @Override
+    public final String getProtocolName() {
+        return PROTOCOL_NAME;
+    }
+
+    public static boolean isCorrectProtocol(PersistableBundle bundle) {
+        return isProtocol(bundle, PROTOCOL_NAME);
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/generic/GenericSpecificationParams.java b/service/support_lib/src/com/google/uwb/support/generic/GenericSpecificationParams.java
new file mode 100644
index 0000000..aee96c5
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/generic/GenericSpecificationParams.java
@@ -0,0 +1,154 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.generic;
+
+import android.annotation.NonNull;
+import android.os.PersistableBundle;
+import android.uwb.UwbManager;
+
+import com.google.uwb.support.base.RequiredParam;
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.ccc.CccSpecificationParams;
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraSpecificationParams;
+
+import java.util.Objects;
+
+/**
+ * Defines parameters for generic capability.
+ *
+ * <p>This is returned as a bundle from the service API {@link UwbManager#getSpecificationInfo}.
+ */
+public class GenericSpecificationParams extends GenericParams {
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    private final FiraSpecificationParams mFiraSpecificationParams;
+    private final CccSpecificationParams mCccSpecificationParams;
+    private final boolean mHasPowerStatsSupport;
+
+    private static final String KEY_FIRA_SPECIFICATION_PARAMS = FiraParams.PROTOCOL_NAME;
+    private static final String KEY_CCC_SPECIFICATION_PARAMS = CccParams.PROTOCOL_NAME;
+    private static final String KEY_POWER_STATS_QUERY_SUPPORT = "power_stats_query";
+
+    private GenericSpecificationParams(
+            FiraSpecificationParams firaSpecificationParams,
+            CccSpecificationParams cccSpecificationParams,
+            boolean hasPowerStatsSupport) {
+        mFiraSpecificationParams = firaSpecificationParams;
+        mCccSpecificationParams = cccSpecificationParams;
+        mHasPowerStatsSupport = hasPowerStatsSupport;
+    }
+
+    @Override
+    protected int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    public FiraSpecificationParams getFiraSpecificationParams() {
+        return mFiraSpecificationParams;
+    }
+
+    public CccSpecificationParams getCccSpecificationParams() {
+        return mCccSpecificationParams;
+    }
+
+    /**
+     * @return if the power stats is supported
+     */
+    public boolean hasPowerStatsSupport() {
+        return mHasPowerStatsSupport;
+    }
+
+    @Override
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = super.toBundle();
+        bundle.putPersistableBundle(KEY_FIRA_SPECIFICATION_PARAMS,
+                mFiraSpecificationParams.toBundle());
+        bundle.putPersistableBundle(KEY_CCC_SPECIFICATION_PARAMS,
+                mCccSpecificationParams.toBundle());
+        bundle.putBoolean(KEY_POWER_STATS_QUERY_SUPPORT, mHasPowerStatsSupport);
+        return bundle;
+    }
+
+    public static GenericSpecificationParams fromBundle(PersistableBundle bundle) {
+        switch (getBundleVersion(bundle)) {
+            case BUNDLE_VERSION_1:
+                return parseVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("Invalid bundle version");
+        }
+    }
+
+    private static GenericSpecificationParams parseVersion1(PersistableBundle bundle) {
+        GenericSpecificationParams.Builder builder = new GenericSpecificationParams.Builder();
+        return builder.setFiraSpecificationParams(
+                        FiraSpecificationParams.fromBundle(
+                                bundle.getPersistableBundle(KEY_FIRA_SPECIFICATION_PARAMS)))
+                .setCccSpecificationParams(
+                        CccSpecificationParams.fromBundle(
+                                bundle.getPersistableBundle(KEY_CCC_SPECIFICATION_PARAMS)))
+                .hasPowerStatsSupport(bundle.getBoolean(KEY_FIRA_SPECIFICATION_PARAMS))
+                .build();
+    }
+
+    /** Builder */
+    public static class Builder {
+        private RequiredParam<FiraSpecificationParams> mFiraSpecificationParams =
+                new RequiredParam<>();
+        private RequiredParam<CccSpecificationParams> mCccSpecificationParams =
+                new RequiredParam<>();
+        private boolean mHasPowerStatsSupport = false;
+
+        /**
+         * Set FIRA specification params
+         */
+        public Builder setFiraSpecificationParams(
+                @NonNull FiraSpecificationParams firaSpecificationParams) {
+            mFiraSpecificationParams.set(Objects.requireNonNull(firaSpecificationParams));
+            return this;
+        }
+
+        /**
+         * Set CCC specification params
+         */
+        public Builder setCccSpecificationParams(
+                @NonNull CccSpecificationParams cccSpecificationParams) {
+            mCccSpecificationParams.set(Objects.requireNonNull(cccSpecificationParams));
+            return this;
+        }
+
+        /**
+         * Sets if the power stats is supported
+         */
+        public Builder hasPowerStatsSupport(boolean value) {
+            mHasPowerStatsSupport = value;
+            return this;
+        }
+
+        /**
+         * Build {@link GenericSpecificationParams}
+         */
+        public GenericSpecificationParams build() {
+            return new GenericSpecificationParams(
+                    mFiraSpecificationParams.get(),
+                    mCccSpecificationParams.get(),
+                    mHasPowerStatsSupport);
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/multichip/ChipInfoParams.java b/service/support_lib/src/com/google/uwb/support/multichip/ChipInfoParams.java
new file mode 100644
index 0000000..3ed6793
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/multichip/ChipInfoParams.java
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.multichip;
+
+import android.os.PersistableBundle;
+
+/**
+ * Defines parameters from return value for  {@link android.uwb.UwbManager#getChipInfos()}.
+ */
+public final class ChipInfoParams {
+    private static final String KEY_CHIP_ID = "KEY_CHIP_ID";
+    private static final String UNKNOWN_CHIP_ID = "UNKNOWN_CHIP_ID";
+
+    private static final String KEY_POSITION_X = "KEY_POSITION_X";
+    private static final String KEY_POSITION_Y = "KEY_POSITION_Y";
+    private static final String KEY_POSITION_Z = "KEY_POSITION_Z";
+
+    private final String mChipId;
+    private final double mPositionX;
+    private final double mPositionY;
+    private final double mPositionZ;
+
+    private ChipInfoParams(String chipId, double positionX, double positionY, double positionZ) {
+        mChipId = chipId;
+        mPositionX = positionX;
+        mPositionY = positionY;
+        mPositionZ = positionZ;
+    }
+
+    /** Returns a String identifier of the chip. */
+    public String getChipId() {
+        return mChipId;
+    }
+
+    /** Returns the x position of the chip as a double in meters. */
+    public double getPositionX() {
+        return mPositionX;
+    }
+
+    /** Returns the y position of the chip as a double in meters. */
+    public double getPositionY() {
+        return mPositionY;
+    }
+
+    /** Returns the z position of the chip as a double in meters. */
+    public double getPositionZ() {
+        return mPositionZ;
+    }
+
+    /** Returns a {@link PersistableBundle} representation of the object. */
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putString(KEY_CHIP_ID, mChipId);
+        bundle.putDouble(KEY_POSITION_X, mPositionX);
+        bundle.putDouble(KEY_POSITION_Y, mPositionY);
+        bundle.putDouble(KEY_POSITION_Z, mPositionZ);
+        return bundle;
+    }
+
+    /** Creates a new {@link ChipInfoParams} from a {@link PersistableBundle}. */
+    public static ChipInfoParams fromBundle(PersistableBundle bundle) {
+        String chipId = bundle.getString(KEY_CHIP_ID, UNKNOWN_CHIP_ID);
+        double positionX = bundle.getDouble(KEY_POSITION_X, 0.0);
+        double positionY = bundle.getDouble(KEY_POSITION_Y, 0.0);
+        double positionZ = bundle.getDouble(KEY_POSITION_Z, 0.0);
+        return new ChipInfoParams(chipId, positionX, positionY, positionZ);
+    }
+
+    /** Creates and returns a {@link Builder}. */
+    public static Builder createBuilder() {
+        return new Builder();
+    }
+
+    /**
+     * A Class for building an object representing the return type of
+     * {@link android.uwb.UwbManager#getChipInfos()}.
+     */
+    public static class Builder {
+        String mChipId = UNKNOWN_CHIP_ID;
+        double mPositionX = 0.0;
+        double mPositionY = 0.0;
+        double mPositionZ = 0.0;
+
+        /** Sets String identifier of chip */
+        public Builder setChipId(String chipId) {
+            mChipId = chipId;
+            return this;
+        }
+
+        /** Sets the x position of the chip measured in meters. */
+        public Builder setPositionX(double positionX) {
+            mPositionX = positionX;
+            return this;
+        }
+
+        /** Sets the y position of the chip measured in meters. */
+        public Builder setPositionY(double positionY) {
+            mPositionY = positionY;
+            return this;
+        }
+
+        /** Sets the z position of the chip measured in meters. */
+        public Builder setPositionZ(double positionZ) {
+            mPositionZ = positionZ;
+            return this;
+        }
+
+        /**
+         * Builds an object representing the return type of
+         * {@link android.uwb.UwbManager#getChipInfos()}.
+         */
+        public ChipInfoParams build()  {
+            return new ChipInfoParams(mChipId, mPositionX, mPositionY, mPositionZ);
+        }
+    }
+}
diff --git a/service/support_lib/src/com/google/uwb/support/profile/ServiceProfile.java b/service/support_lib/src/com/google/uwb/support/profile/ServiceProfile.java
new file mode 100644
index 0000000..6202c10
--- /dev/null
+++ b/service/support_lib/src/com/google/uwb/support/profile/ServiceProfile.java
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support.profile;
+
+import android.os.PersistableBundle;
+
+import com.google.uwb.support.base.RequiredParam;
+import com.google.uwb.support.fira.FiraParams.ServiceID;
+
+/** UWB service configuration for FiRa profile. */
+public class ServiceProfile {
+    private static final int BUNDLE_VERSION_1 = 1;
+    private static final int BUNDLE_VERSION_CURRENT = BUNDLE_VERSION_1;
+
+    @ServiceID
+    private final int mServiceID;
+    public static final String KEY_BUNDLE_VERSION = "bundle_version";
+    public static final String SERVICE_ID = "service_id";
+
+    public static int getBundleVersion() {
+        return BUNDLE_VERSION_CURRENT;
+    }
+
+    @ServiceID
+    public int getServiceID() {
+        return mServiceID;
+    }
+
+    public ServiceProfile(int serviceID) {
+        mServiceID = serviceID;
+    }
+
+    public PersistableBundle toBundle() {
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putInt(KEY_BUNDLE_VERSION, getBundleVersion());
+        bundle.putInt(SERVICE_ID, mServiceID);
+        return bundle;
+    }
+
+    public static ServiceProfile fromBundle(PersistableBundle bundle) {
+        switch (bundle.getInt(KEY_BUNDLE_VERSION)) {
+            case BUNDLE_VERSION_1:
+                return parseVersion1(bundle);
+
+            default:
+                throw new IllegalArgumentException("Invalid bundle version");
+        }
+    }
+
+    private static ServiceProfile parseVersion1(PersistableBundle bundle) {
+        return new ServiceProfile.Builder()
+                .setServiceID(bundle.getInt(SERVICE_ID))
+                .build();
+    }
+
+    /** Builder */
+    public static class Builder {
+        private final RequiredParam<Integer> mServiceID = new RequiredParam<>();
+
+        public ServiceProfile.Builder setServiceID(int serviceID) {
+            mServiceID.set(serviceID);
+            return this;
+        }
+
+        public ServiceProfile build() {
+            return new ServiceProfile(
+                    mServiceID.get());
+        }
+    }
+}
diff --git a/service/support_lib/test/Android.bp b/service/support_lib/test/Android.bp
new file mode 100644
index 0000000..49dceaf
--- /dev/null
+++ b/service/support_lib/test/Android.bp
@@ -0,0 +1,19 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "UwbSupportLibTests",
+    srcs: ["*.java"],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "com.uwb.support.ccc",
+        "com.uwb.support.fira",
+        "com.uwb.support.generic",
+        "com.uwb.support.multichip",
+    ],
+    platform_apis: true,
+    certificate: "platform",
+    test_suites: ["device-tests"],
+}
diff --git a/service/support_lib/test/AndroidManifest.xml b/service/support_lib/test/AndroidManifest.xml
new file mode 100644
index 0000000..c3b0746
--- /dev/null
+++ b/service/support_lib/test/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.google.uwb.support">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <!-- This is a self-instrumenting test package. -->
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:label="UWB Support Lib Tests"
+        android:targetPackage="com.google.uwb.support" />
+
+</manifest>
\ No newline at end of file
diff --git a/service/support_lib/test/AndroidTest.xml b/service/support_lib/test/AndroidTest.xml
new file mode 100644
index 0000000..c0a9c23
--- /dev/null
+++ b/service/support_lib/test/AndroidTest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration description="Config for UWB Support Lib test cases">
+    <option name="test-suite-tag" value="apct"/>
+    <option name="test-suite-tag" value="apct-instrumentation"/>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="UwbSupportLibTests.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct"/>
+    <option name="test-tag" value="UwbSupportLibTests"/>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.google.uwb.support" />
+        <option name="hidden-api-checks" value="false"/>
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/>
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/service/support_lib/test/CccTests.java b/service/support_lib/test/CccTests.java
new file mode 100644
index 0000000..e9a0fde
--- /dev/null
+++ b/service/support_lib/test/CccTests.java
@@ -0,0 +1,309 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.os.PersistableBundle;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.ccc.CccOpenRangingParams;
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.ccc.CccProtocolVersion;
+import com.google.uwb.support.ccc.CccPulseShapeCombo;
+import com.google.uwb.support.ccc.CccRangingError;
+import com.google.uwb.support.ccc.CccRangingReconfiguredParams;
+import com.google.uwb.support.ccc.CccRangingStartedParams;
+import com.google.uwb.support.ccc.CccSpecificationParams;
+import com.google.uwb.support.ccc.CccStartRangingParams;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CccTests {
+
+    @Test
+    public void testOpenRangingParams() {
+        CccProtocolVersion protocolVersion = CccParams.PROTOCOL_VERSION_1_0;
+        @CccParams.UwbConfig int uwbConfig = CccParams.UWB_CONFIG_1;
+        CccPulseShapeCombo pulseShapeCombo =
+                new CccPulseShapeCombo(
+                        CccParams.PULSE_SHAPE_PRECURSOR_FREE, CccParams.PULSE_SHAPE_PRECURSOR_FREE);
+        int sessionId = 10;
+        int ranMultiplier = 128;
+        @CccParams.Channel int channel = CccParams.UWB_CHANNEL_9;
+        @CccParams.ChapsPerSlot int chapsPerSlot = CccParams.CHAPS_PER_SLOT_6;
+        int numResponderNodes = 9;
+        @CccParams.SlotsPerRound int numSlotsPerRound = CccParams.SLOTS_PER_ROUND_12;
+        @CccParams.SyncCodeIndex int syncCodeIdx = 22;
+        @CccParams.HoppingConfigMode int hoppingConfigMode = CccParams.HOPPING_CONFIG_MODE_ADAPTIVE;
+        @CccParams.HoppingSequence int hoppingSequence = CccParams.HOPPING_SEQUENCE_AES;
+
+        CccOpenRangingParams params =
+                new CccOpenRangingParams.Builder()
+                        .setProtocolVersion(protocolVersion)
+                        .setUwbConfig(uwbConfig)
+                        .setPulseShapeCombo(pulseShapeCombo)
+                        .setSessionId(sessionId)
+                        .setRanMultiplier(ranMultiplier)
+                        .setChannel(channel)
+                        .setNumChapsPerSlot(chapsPerSlot)
+                        .setNumResponderNodes(numResponderNodes)
+                        .setNumSlotsPerRound(numSlotsPerRound)
+                        .setSyncCodeIndex(syncCodeIdx)
+                        .setHoppingConfigMode(hoppingConfigMode)
+                        .setHoppingSequence(hoppingSequence)
+                        .build();
+
+        assertEquals(params.getProtocolVersion(), protocolVersion);
+        assertEquals(params.getUwbConfig(), uwbConfig);
+        assertEquals(
+                params.getPulseShapeCombo().getInitiatorTx(), pulseShapeCombo.getInitiatorTx());
+        assertEquals(
+                params.getPulseShapeCombo().getResponderTx(), pulseShapeCombo.getResponderTx());
+        assertEquals(params.getSessionId(), sessionId);
+        assertEquals(params.getRanMultiplier(), ranMultiplier);
+        assertEquals(params.getChannel(), channel);
+        assertEquals(params.getNumChapsPerSlot(), chapsPerSlot);
+        assertEquals(params.getNumResponderNodes(), numResponderNodes);
+        assertEquals(params.getNumSlotsPerRound(), numSlotsPerRound);
+        assertEquals(params.getSyncCodeIndex(), syncCodeIdx);
+        assertEquals(params.getHoppingConfigMode(), hoppingConfigMode);
+        assertEquals(params.getHoppingSequence(), hoppingSequence);
+
+        CccOpenRangingParams fromBundle = CccOpenRangingParams.fromBundle(params.toBundle());
+        assertEquals(fromBundle.getProtocolVersion(), protocolVersion);
+        assertEquals(fromBundle.getUwbConfig(), uwbConfig);
+        assertEquals(
+                fromBundle.getPulseShapeCombo().getInitiatorTx(), pulseShapeCombo.getInitiatorTx());
+        assertEquals(
+                fromBundle.getPulseShapeCombo().getResponderTx(), pulseShapeCombo.getResponderTx());
+        assertEquals(fromBundle.getSessionId(), sessionId);
+        assertEquals(fromBundle.getRanMultiplier(), ranMultiplier);
+        assertEquals(fromBundle.getChannel(), channel);
+        assertEquals(fromBundle.getNumChapsPerSlot(), chapsPerSlot);
+        assertEquals(fromBundle.getNumResponderNodes(), numResponderNodes);
+        assertEquals(fromBundle.getNumSlotsPerRound(), numSlotsPerRound);
+        assertEquals(fromBundle.getSyncCodeIndex(), syncCodeIdx);
+        assertEquals(fromBundle.getHoppingConfigMode(), hoppingConfigMode);
+        assertEquals(fromBundle.getHoppingSequence(), hoppingSequence);
+
+        verifyProtocolPresent(params);
+        verifyBundlesEqual(params, fromBundle);
+    }
+
+    @Test
+    public void testRangingError() {
+        @CccParams.ProtocolError int error = CccParams.PROTOCOL_ERROR_SE_BUSY;
+        CccRangingError params = new CccRangingError.Builder().setError(error).build();
+
+        assertEquals(params.getError(), error);
+
+        CccRangingError fromBundle = CccRangingError.fromBundle(params.toBundle());
+        assertEquals(fromBundle.getError(), error);
+
+        verifyProtocolPresent(params);
+        verifyBundlesEqual(params, fromBundle);
+    }
+
+    @Test
+    public void testRangingReconfiguredParams() {
+        CccRangingReconfiguredParams params = new CccRangingReconfiguredParams.Builder().build();
+
+        CccRangingReconfiguredParams fromBundle =
+                CccRangingReconfiguredParams.fromBundle(params.toBundle());
+
+        verifyProtocolPresent(params);
+        verifyBundlesEqual(params, fromBundle);
+    }
+
+    @Test
+    public void testStartRangingParams() {
+        int sessionId = 10;
+        int ranMultiplier = 128;
+
+        CccStartRangingParams params =
+                new CccStartRangingParams.Builder()
+                        .setSessionId(sessionId)
+                        .setRanMultiplier(ranMultiplier)
+                        .build();
+
+        assertEquals(params.getSessionId(), sessionId);
+        assertEquals(params.getRanMultiplier(), ranMultiplier);
+
+        CccStartRangingParams fromBundle = CccStartRangingParams.fromBundle(params.toBundle());
+
+        assertEquals(fromBundle.getSessionId(), sessionId);
+        assertEquals(fromBundle.getRanMultiplier(), ranMultiplier);
+
+        verifyProtocolPresent(params);
+        verifyBundlesEqual(params, fromBundle);
+    }
+
+    @Test
+    public void testRangingStartedParams() {
+        int hopModeKey = 98876444;
+        int startingStsIndex = 246802468;
+        @CccParams.SyncCodeIndex int syncCodeIndex = 10;
+        long uwbTime0 = 50;
+        int ranMultiplier = 10;
+
+        CccRangingStartedParams params =
+                new CccRangingStartedParams.Builder()
+                        .setHopModeKey(hopModeKey)
+                        .setStartingStsIndex(startingStsIndex)
+                        .setSyncCodeIndex(syncCodeIndex)
+                        .setUwbTime0(uwbTime0)
+                        .setRanMultiplier(ranMultiplier)
+                        .build();
+
+        assertEquals(params.getHopModeKey(), hopModeKey);
+        assertEquals(params.getStartingStsIndex(), startingStsIndex);
+        assertEquals(params.getSyncCodeIndex(), syncCodeIndex);
+        assertEquals(params.getUwbTime0(), uwbTime0);
+        assertEquals(params.getRanMultiplier(), ranMultiplier);
+
+        CccRangingStartedParams fromBundle = CccRangingStartedParams.fromBundle(params.toBundle());
+
+        assertEquals(fromBundle.getHopModeKey(), hopModeKey);
+        assertEquals(fromBundle.getStartingStsIndex(), startingStsIndex);
+        assertEquals(fromBundle.getSyncCodeIndex(), syncCodeIndex);
+        assertEquals(fromBundle.getUwbTime0(), uwbTime0);
+        assertEquals(fromBundle.getRanMultiplier(), ranMultiplier);
+
+        verifyProtocolPresent(params);
+        verifyBundlesEqual(params, fromBundle);
+    }
+
+    @Test
+    public void testSpecificationParams() {
+        CccProtocolVersion[] protocolVersions =
+                new CccProtocolVersion[] {
+                    new CccProtocolVersion(1, 0),
+                    new CccProtocolVersion(2, 0),
+                    new CccProtocolVersion(2, 1)
+                };
+
+        Integer[] uwbConfigs = new Integer[] {CccParams.UWB_CONFIG_0, CccParams.UWB_CONFIG_1};
+        CccPulseShapeCombo[] pulseShapeCombos =
+                new CccPulseShapeCombo[] {
+                    new CccPulseShapeCombo(
+                            CccParams.PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE,
+                            CccParams.PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE),
+                    new CccPulseShapeCombo(
+                            CccParams.PULSE_SHAPE_PRECURSOR_FREE,
+                            CccParams.PULSE_SHAPE_PRECURSOR_FREE),
+                    new CccPulseShapeCombo(
+                            CccParams.PULSE_SHAPE_PRECURSOR_FREE_SPECIAL,
+                            CccParams.PULSE_SHAPE_PRECURSOR_FREE_SPECIAL)
+                };
+        int ranMultiplier = 200;
+        Integer[] chapsPerSlots =
+                new Integer[] {CccParams.CHAPS_PER_SLOT_4, CccParams.CHAPS_PER_SLOT_12};
+        Integer[] syncCodes =
+                new Integer[] {10, 23};
+        Integer[] channels = new Integer[] {CccParams.UWB_CHANNEL_5, CccParams.UWB_CHANNEL_9};
+        Integer[] hoppingConfigModes =
+                new Integer[] {
+                    CccParams.HOPPING_CONFIG_MODE_ADAPTIVE, CccParams.HOPPING_CONFIG_MODE_CONTINUOUS
+                };
+        Integer[] hoppingSequences =
+                new Integer[] {CccParams.HOPPING_SEQUENCE_AES, CccParams.HOPPING_SEQUENCE_DEFAULT};
+
+        CccSpecificationParams.Builder paramsBuilder = new CccSpecificationParams.Builder();
+        for (CccProtocolVersion p : protocolVersions) {
+            paramsBuilder.addProtocolVersion(p);
+        }
+
+        for (int uwbConfig : uwbConfigs) {
+            paramsBuilder.addUwbConfig(uwbConfig);
+        }
+
+        for (CccPulseShapeCombo pulseShapeCombo : pulseShapeCombos) {
+            paramsBuilder.addPulseShapeCombo(pulseShapeCombo);
+        }
+
+        paramsBuilder.setRanMultiplier(ranMultiplier);
+
+        for (int chapsPerSlot : chapsPerSlots) {
+            paramsBuilder.addChapsPerSlot(chapsPerSlot);
+        }
+
+        for (int syncCode : syncCodes) {
+            paramsBuilder.addSyncCode(syncCode);
+        }
+
+        for (int channel : channels) {
+            paramsBuilder.addChannel(channel);
+        }
+
+        for (int hoppingConfigMode : hoppingConfigModes) {
+            paramsBuilder.addHoppingConfigMode(hoppingConfigMode);
+        }
+
+        for (int hoppingSequence : hoppingSequences) {
+            paramsBuilder.addHoppingSequence(hoppingSequence);
+        }
+
+        CccSpecificationParams params = paramsBuilder.build();
+        assertArrayEquals(params.getProtocolVersions().toArray(), protocolVersions);
+        assertArrayEquals(params.getUwbConfigs().toArray(), uwbConfigs);
+        assertArrayEquals(params.getPulseShapeCombos().toArray(), pulseShapeCombos);
+        assertEquals(params.getRanMultiplier(), ranMultiplier);
+        assertArrayEquals(params.getChapsPerSlot().toArray(), chapsPerSlots);
+        assertArrayEquals(params.getSyncCodes().toArray(), syncCodes);
+        assertArrayEquals(params.getChannels().toArray(), channels);
+        assertArrayEquals(params.getHoppingConfigModes().toArray(), hoppingConfigModes);
+        assertArrayEquals(params.getHoppingSequences().toArray(), hoppingSequences);
+
+        CccSpecificationParams fromBundle = CccSpecificationParams.fromBundle(params.toBundle());
+        assertArrayEquals(fromBundle.getProtocolVersions().toArray(), protocolVersions);
+        assertArrayEquals(fromBundle.getUwbConfigs().toArray(), uwbConfigs);
+        assertArrayEquals(fromBundle.getPulseShapeCombos().toArray(), pulseShapeCombos);
+        assertEquals(fromBundle.getRanMultiplier(), ranMultiplier);
+        assertArrayEquals(fromBundle.getChapsPerSlot().toArray(), chapsPerSlots);
+        assertArrayEquals(fromBundle.getSyncCodes().toArray(), syncCodes);
+        assertArrayEquals(fromBundle.getChannels().toArray(), channels);
+        assertArrayEquals(fromBundle.getHoppingConfigModes().toArray(), hoppingConfigModes);
+        assertArrayEquals(fromBundle.getHoppingSequences().toArray(), hoppingSequences);
+
+        verifyProtocolPresent(params);
+        assertTrue(params.equals(fromBundle));
+
+        // Add random channel to params builder to force inequality.
+        paramsBuilder.addChannel(0);
+        // Rebuild params.
+        params = paramsBuilder.build();
+        // Test that params and fromBundle are not equal.
+        assertTrue(!params.equals(fromBundle));
+    }
+
+    private void verifyProtocolPresent(Params params) {
+        assertTrue(Params.isProtocol(params.toBundle(), CccParams.PROTOCOL_NAME));
+    }
+
+    private void verifyBundlesEqual(Params params, Params fromBundle) {
+        assertTrue(PersistableBundle.kindofEquals(params.toBundle(), fromBundle.toBundle()));
+    }
+}
diff --git a/service/support_lib/test/FiraTests.java b/service/support_lib/test/FiraTests.java
new file mode 100644
index 0000000..8343169
--- /dev/null
+++ b/service/support_lib/test/FiraTests.java
@@ -0,0 +1,522 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support;
+
+import static com.google.uwb.support.fira.FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_INTERLEAVED;
+import static com.google.uwb.support.fira.FiraParams.AOA_TYPE_AZIMUTH_AND_ELEVATION;
+import static com.google.uwb.support.fira.FiraParams.BPRF_PHR_DATA_RATE_6M81;
+import static com.google.uwb.support.fira.FiraParams.MAC_ADDRESS_MODE_8_BYTES;
+import static com.google.uwb.support.fira.FiraParams.MAC_FCS_TYPE_CRC_32;
+import static com.google.uwb.support.fira.FiraParams.MEASUREMENT_REPORT_TYPE_INITIATOR_TO_RESPONDER;
+import static com.google.uwb.support.fira.FiraParams.MULTICAST_LIST_UPDATE_ACTION_DELETE;
+import static com.google.uwb.support.fira.FiraParams.MULTICAST_LIST_UPDATE_STATUS_ERROR_MULTICAST_LIST_FULL;
+import static com.google.uwb.support.fira.FiraParams.MULTI_NODE_MODE_MANY_TO_MANY;
+import static com.google.uwb.support.fira.FiraParams.PREAMBLE_DURATION_T32_SYMBOLS;
+import static com.google.uwb.support.fira.FiraParams.PRF_MODE_HPRF;
+import static com.google.uwb.support.fira.FiraParams.PSDU_DATA_RATE_7M80;
+import static com.google.uwb.support.fira.FiraParams.RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY;
+import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_ROLE_INITIATOR;
+import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_TYPE_CONTROLEE;
+import static com.google.uwb.support.fira.FiraParams.RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE;
+import static com.google.uwb.support.fira.FiraParams.RFRAME_CONFIG_SP1;
+import static com.google.uwb.support.fira.FiraParams.SFD_ID_VALUE_3;
+import static com.google.uwb.support.fira.FiraParams.STATE_CHANGE_REASON_CODE_ERROR_INVALID_RANGING_INTERVAL;
+import static com.google.uwb.support.fira.FiraParams.STATUS_CODE_ERROR_ADDRESS_ALREADY_PRESENT;
+import static com.google.uwb.support.fira.FiraParams.STS_CONFIG_DYNAMIC_FOR_CONTROLEE_INDIVIDUAL_KEY;
+import static com.google.uwb.support.fira.FiraParams.STS_LENGTH_128_SYMBOLS;
+import static com.google.uwb.support.fira.FiraParams.STS_SEGMENT_COUNT_VALUE_2;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.os.PersistableBundle;
+import android.uwb.UwbAddress;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.fira.FiraControleeParams;
+import com.google.uwb.support.fira.FiraMulticastListUpdateStatusCode;
+import com.google.uwb.support.fira.FiraOpenSessionParams;
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraProtocolVersion;
+import com.google.uwb.support.fira.FiraRangingReconfigureParams;
+import com.google.uwb.support.fira.FiraSpecificationParams;
+import com.google.uwb.support.fira.FiraStateChangeReasonCode;
+import com.google.uwb.support.fira.FiraStatusCode;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class FiraTests {
+    @Test
+    public void testOpenSessionParams() {
+        FiraProtocolVersion protocolVersion = FiraParams.PROTOCOL_VERSION_1_1;
+        int sessionId = 10;
+        int deviceType = RANGING_DEVICE_TYPE_CONTROLEE;
+        int deviceRole = RANGING_DEVICE_ROLE_INITIATOR;
+        int rangingRoundUsage = RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE;
+        int multiNodeMode = MULTI_NODE_MODE_MANY_TO_MANY;
+        int addressMode = MAC_ADDRESS_MODE_8_BYTES;
+        UwbAddress deviceAddress = UwbAddress.fromBytes(new byte[] {1, 2, 3, 4, 5, 6, 7, 8});
+        UwbAddress destAddress1 = UwbAddress.fromBytes(new byte[] {1, 2, 3, 4, 5, 6, 7, 8});
+        UwbAddress destAddress2 =
+                UwbAddress.fromBytes(new byte[] {(byte) 0xFF, (byte) 0xFE, 3, 4, 5, 6, 7, 8});
+        List<UwbAddress> destAddressList = new ArrayList<>();
+        destAddressList.add(destAddress1);
+        destAddressList.add(destAddress2);
+        int initiationTimeMs = 100;
+        int slotDurationRstu = 2400;
+        int slotsPerRangingRound = 10;
+        int rangingIntervalMs = 100;
+        int blockStrideLength = 2;
+        int maxRangingRoundRetries = 3;
+        int sessionPriority = 100;
+        boolean hasResultReportPhase = true;
+        int measurementReportType = MEASUREMENT_REPORT_TYPE_INITIATOR_TO_RESPONDER;
+        int inBandTerminationAttemptCount = 8;
+        int channelNumber = 10;
+        int preambleCodeIndex = 12;
+        int rframeConfig = RFRAME_CONFIG_SP1;
+        int prfMode = PRF_MODE_HPRF;
+        int preambleDuration = PREAMBLE_DURATION_T32_SYMBOLS;
+        int sfdId = SFD_ID_VALUE_3;
+        int stsSegmentCount = STS_SEGMENT_COUNT_VALUE_2;
+        int stsLength = STS_LENGTH_128_SYMBOLS;
+        int psduDataRate = PSDU_DATA_RATE_7M80;
+        int bprfPhrDataRate = BPRF_PHR_DATA_RATE_6M81;
+        int fcsType = MAC_FCS_TYPE_CRC_32;
+        boolean isTxAdaptivePayloadPowerEnabled = true;
+        int stsConfig = STS_CONFIG_DYNAMIC_FOR_CONTROLEE_INDIVIDUAL_KEY;
+        int subSessionId = 24;
+        byte[] vendorId = new byte[] {(byte) 0xFE, (byte) 0xDC};
+        byte[] staticStsIV = new byte[] {(byte) 0xDF, (byte) 0xCE, (byte) 0xAB, 0x12, 0x34, 0x56};
+        boolean isKeyRotationEnabled = true;
+        int keyRotationRate = 15;
+        int aoaResultRequest = AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_INTERLEAVED;
+        int rangeDataNtfConfig = RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY;
+        int rangeDataNtfProximityNear = 50;
+        int rangeDataNtfProximityFar = 200;
+        boolean hasTimeOfFlightReport = true;
+        boolean hasAngleOfArrivalAzimuthReport = true;
+        boolean hasAngleOfArrivalElevationReport = true;
+        boolean hasAngleOfArrivalFigureOfMeritReport = true;
+        int aoaType = AOA_TYPE_AZIMUTH_AND_ELEVATION;
+        int numOfMsrmtFocusOnRange = 1;
+        int numOfMsrmtFocusOnAoaAzimuth = 2;
+        int numOfMsrmtFocusOnAoaElevation = 3;
+
+        FiraOpenSessionParams params =
+                new FiraOpenSessionParams.Builder()
+                        .setProtocolVersion(protocolVersion)
+                        .setSessionId(sessionId)
+                        .setDeviceType(deviceType)
+                        .setDeviceRole(deviceRole)
+                        .setRangingRoundUsage(rangingRoundUsage)
+                        .setMultiNodeMode(multiNodeMode)
+                        .setDeviceAddress(deviceAddress)
+                        .setDestAddressList(destAddressList)
+                        .setInitiationTimeMs(initiationTimeMs)
+                        .setSlotDurationRstu(slotDurationRstu)
+                        .setSlotsPerRangingRound(slotsPerRangingRound)
+                        .setRangingIntervalMs(rangingIntervalMs)
+                        .setBlockStrideLength(blockStrideLength)
+                        .setMaxRangingRoundRetries(maxRangingRoundRetries)
+                        .setSessionPriority(sessionPriority)
+                        .setMacAddressMode(addressMode)
+                        .setHasResultReportPhase(hasResultReportPhase)
+                        .setMeasurementReportType(measurementReportType)
+                        .setInBandTerminationAttemptCount(inBandTerminationAttemptCount)
+                        .setChannelNumber(channelNumber)
+                        .setPreambleCodeIndex(preambleCodeIndex)
+                        .setRframeConfig(rframeConfig)
+                        .setPrfMode(prfMode)
+                        .setPreambleDuration(preambleDuration)
+                        .setSfdId(sfdId)
+                        .setStsSegmentCount(stsSegmentCount)
+                        .setStsLength(stsLength)
+                        .setPsduDataRate(psduDataRate)
+                        .setBprfPhrDataRate(bprfPhrDataRate)
+                        .setFcsType(fcsType)
+                        .setIsTxAdaptivePayloadPowerEnabled(isTxAdaptivePayloadPowerEnabled)
+                        .setStsConfig(stsConfig)
+                        .setSubSessionId(subSessionId)
+                        .setVendorId(vendorId)
+                        .setStaticStsIV(staticStsIV)
+                        .setIsKeyRotationEnabled(isKeyRotationEnabled)
+                        .setKeyRotationRate(keyRotationRate)
+                        .setAoaResultRequest(aoaResultRequest)
+                        .setRangeDataNtfConfig(rangeDataNtfConfig)
+                        .setRangeDataNtfProximityNear(rangeDataNtfProximityNear)
+                        .setRangeDataNtfProximityFar(rangeDataNtfProximityFar)
+                        .setHasTimeOfFlightReport(hasTimeOfFlightReport)
+                        .setHasAngleOfArrivalAzimuthReport(hasAngleOfArrivalAzimuthReport)
+                        .setHasAngleOfArrivalElevationReport(hasAngleOfArrivalElevationReport)
+                        .setHasAngleOfArrivalFigureOfMeritReport(
+                                hasAngleOfArrivalFigureOfMeritReport)
+                        .setAoaType(aoaType)
+                        .setMeasurementFocusRatio(
+                                numOfMsrmtFocusOnRange,
+                                numOfMsrmtFocusOnAoaAzimuth,
+                                numOfMsrmtFocusOnAoaElevation)
+                        .build();
+
+        assertEquals(params.getProtocolVersion(), protocolVersion);
+        assertEquals(params.getSessionId(), sessionId);
+        assertEquals(params.getDeviceType(), deviceType);
+        assertEquals(params.getDeviceRole(), deviceRole);
+        assertEquals(params.getRangingRoundUsage(), rangingRoundUsage);
+        assertEquals(params.getMultiNodeMode(), multiNodeMode);
+        assertEquals(params.getDeviceAddress(), deviceAddress);
+        assertEquals(params.getDestAddressList().size(), destAddressList.size());
+        for (int i = 0; i < destAddressList.size(); i++) {
+            assertEquals(params.getDestAddressList().get(i), destAddressList.get(i));
+        }
+
+        assertEquals(params.getInitiationTimeMs(), initiationTimeMs);
+        assertEquals(params.getSlotDurationRstu(), slotDurationRstu);
+        assertEquals(params.getSlotsPerRangingRound(), slotsPerRangingRound);
+        assertEquals(params.getRangingIntervalMs(), rangingIntervalMs);
+        assertEquals(params.getBlockStrideLength(), blockStrideLength);
+        assertEquals(params.getMaxRangingRoundRetries(), maxRangingRoundRetries);
+        assertEquals(params.getSessionPriority(), sessionPriority);
+        assertEquals(params.getMacAddressMode(), addressMode);
+        assertEquals(params.hasResultReportPhase(), hasResultReportPhase);
+        assertEquals(params.getMeasurementReportType(), measurementReportType);
+        assertEquals(params.getInBandTerminationAttemptCount(), inBandTerminationAttemptCount);
+        assertEquals(params.getChannelNumber(), channelNumber);
+        assertEquals(params.getPreambleCodeIndex(), preambleCodeIndex);
+        assertEquals(params.getRframeConfig(), rframeConfig);
+        assertEquals(params.getPrfMode(), prfMode);
+        assertEquals(params.getPreambleDuration(), preambleDuration);
+        assertEquals(params.getSfdId(), sfdId);
+        assertEquals(params.getStsSegmentCount(), stsSegmentCount);
+        assertEquals(params.getStsLength(), stsLength);
+        assertEquals(params.getPsduDataRate(), psduDataRate);
+        assertEquals(params.getBprfPhrDataRate(), bprfPhrDataRate);
+        assertEquals(params.getFcsType(), fcsType);
+        assertEquals(params.isTxAdaptivePayloadPowerEnabled(), isTxAdaptivePayloadPowerEnabled);
+        assertEquals(params.getStsConfig(), stsConfig);
+        assertEquals(params.getSubSessionId(), subSessionId);
+        assertArrayEquals(params.getVendorId(), vendorId);
+        assertArrayEquals(params.getStaticStsIV(), staticStsIV);
+        assertEquals(params.isKeyRotationEnabled(), isKeyRotationEnabled);
+        assertEquals(params.getKeyRotationRate(), keyRotationRate);
+        assertEquals(params.getAoaResultRequest(), aoaResultRequest);
+        assertEquals(params.getRangeDataNtfConfig(), rangeDataNtfConfig);
+        assertEquals(params.getRangeDataNtfProximityNear(), rangeDataNtfProximityNear);
+        assertEquals(params.getRangeDataNtfProximityFar(), rangeDataNtfProximityFar);
+        assertEquals(params.hasTimeOfFlightReport(), hasTimeOfFlightReport);
+        assertEquals(params.hasAngleOfArrivalAzimuthReport(), hasAngleOfArrivalAzimuthReport);
+        assertEquals(params.hasAngleOfArrivalElevationReport(), hasAngleOfArrivalElevationReport);
+        assertEquals(
+                params.hasAngleOfArrivalFigureOfMeritReport(),
+                hasAngleOfArrivalFigureOfMeritReport);
+        assertEquals(params.getAoaType(), aoaType);
+        assertEquals(params.getNumOfMsrmtFocusOnRange(), numOfMsrmtFocusOnRange);
+        assertEquals(params.getNumOfMsrmtFocusOnAoaAzimuth(), numOfMsrmtFocusOnAoaAzimuth);
+        assertEquals(params.getNumOfMsrmtFocusOnAoaElevation(), numOfMsrmtFocusOnAoaElevation);
+
+        FiraOpenSessionParams fromBundle = FiraOpenSessionParams.fromBundle(params.toBundle());
+
+        assertEquals(fromBundle.getRangingRoundUsage(), rangingRoundUsage);
+        assertEquals(fromBundle.getMultiNodeMode(), multiNodeMode);
+
+        assertEquals(fromBundle.getDeviceAddress(), deviceAddress);
+        assertEquals(fromBundle.getDestAddressList().size(), destAddressList.size());
+        for (int i = 0; i < destAddressList.size(); i++) {
+            assertEquals(fromBundle.getDestAddressList().get(i), destAddressList.get(i));
+        }
+
+        assertEquals(fromBundle.getInitiationTimeMs(), initiationTimeMs);
+        assertEquals(fromBundle.getSlotDurationRstu(), slotDurationRstu);
+        assertEquals(fromBundle.getSlotsPerRangingRound(), slotsPerRangingRound);
+        assertEquals(fromBundle.getRangingIntervalMs(), rangingIntervalMs);
+        assertEquals(fromBundle.getBlockStrideLength(), blockStrideLength);
+        assertEquals(fromBundle.getMaxRangingRoundRetries(), maxRangingRoundRetries);
+        assertEquals(fromBundle.getSessionPriority(), sessionPriority);
+        assertEquals(fromBundle.getMacAddressMode(), addressMode);
+        assertEquals(fromBundle.hasResultReportPhase(), hasResultReportPhase);
+        assertEquals(fromBundle.getMeasurementReportType(), measurementReportType);
+        assertEquals(fromBundle.getInBandTerminationAttemptCount(), inBandTerminationAttemptCount);
+        assertEquals(fromBundle.getChannelNumber(), channelNumber);
+        assertEquals(fromBundle.getPreambleCodeIndex(), preambleCodeIndex);
+        assertEquals(fromBundle.getRframeConfig(), rframeConfig);
+        assertEquals(fromBundle.getPrfMode(), prfMode);
+        assertEquals(fromBundle.getPreambleDuration(), preambleDuration);
+        assertEquals(fromBundle.getSfdId(), sfdId);
+        assertEquals(fromBundle.getStsSegmentCount(), stsSegmentCount);
+        assertEquals(fromBundle.getStsLength(), stsLength);
+        assertEquals(fromBundle.getPsduDataRate(), psduDataRate);
+        assertEquals(fromBundle.getBprfPhrDataRate(), bprfPhrDataRate);
+        assertEquals(fromBundle.getFcsType(), fcsType);
+        assertEquals(fromBundle.isTxAdaptivePayloadPowerEnabled(), isTxAdaptivePayloadPowerEnabled);
+        assertEquals(fromBundle.getStsConfig(), stsConfig);
+        assertEquals(fromBundle.getSubSessionId(), subSessionId);
+        assertArrayEquals(fromBundle.getVendorId(), vendorId);
+        assertArrayEquals(fromBundle.getStaticStsIV(), staticStsIV);
+        assertEquals(fromBundle.isKeyRotationEnabled(), isKeyRotationEnabled);
+        assertEquals(fromBundle.getKeyRotationRate(), keyRotationRate);
+        assertEquals(fromBundle.getAoaResultRequest(), aoaResultRequest);
+        assertEquals(fromBundle.getRangeDataNtfConfig(), rangeDataNtfConfig);
+        assertEquals(fromBundle.getRangeDataNtfProximityNear(), rangeDataNtfProximityNear);
+        assertEquals(fromBundle.getRangeDataNtfProximityFar(), rangeDataNtfProximityFar);
+        assertEquals(fromBundle.hasTimeOfFlightReport(), hasTimeOfFlightReport);
+        assertEquals(fromBundle.hasAngleOfArrivalAzimuthReport(), hasAngleOfArrivalAzimuthReport);
+        assertEquals(
+                fromBundle.hasAngleOfArrivalElevationReport(), hasAngleOfArrivalElevationReport);
+        assertEquals(
+                fromBundle.hasAngleOfArrivalFigureOfMeritReport(),
+                hasAngleOfArrivalFigureOfMeritReport);
+        assertEquals(fromBundle.getAoaType(), aoaType);
+        assertEquals(fromBundle.getNumOfMsrmtFocusOnRange(), numOfMsrmtFocusOnRange);
+        assertEquals(fromBundle.getNumOfMsrmtFocusOnAoaAzimuth(), numOfMsrmtFocusOnAoaAzimuth);
+        assertEquals(fromBundle.getNumOfMsrmtFocusOnAoaElevation(), numOfMsrmtFocusOnAoaElevation);
+
+        verifyProtocolPresent(params);
+        verifyBundlesEqual(params, fromBundle);
+    }
+
+    @Test
+    public void testRangingReconfigureParams() {
+        int action = MULTICAST_LIST_UPDATE_ACTION_DELETE;
+        UwbAddress uwbAddress1 = UwbAddress.fromBytes(new byte[] {1, 2});
+        UwbAddress uwbAddress2 = UwbAddress.fromBytes(new byte[] {4, 5});
+        UwbAddress[] addressList = new UwbAddress[] {uwbAddress1, uwbAddress2};
+        int blockStrideLength = 5;
+        int rangeDataNtfConfig = RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY;
+        int rangeDataProximityNear = 100;
+        int rangeDataProximityFar = 500;
+
+        int[] subSessionIdList = new int[] {3, 4};
+        FiraRangingReconfigureParams params =
+                new FiraRangingReconfigureParams.Builder()
+                        .setAction(action)
+                        .setAddressList(addressList)
+                        .setSubSessionIdList(subSessionIdList)
+                        .build();
+
+        assertEquals((int) params.getAction(), action);
+        assertArrayEquals(params.getAddressList(), addressList);
+        assertArrayEquals(params.getSubSessionIdList(), subSessionIdList);
+        FiraRangingReconfigureParams fromBundle =
+                FiraRangingReconfigureParams.fromBundle(params.toBundle());
+        assertEquals((int) fromBundle.getAction(), action);
+        assertArrayEquals(fromBundle.getAddressList(), addressList);
+        assertArrayEquals(fromBundle.getSubSessionIdList(), subSessionIdList);
+        verifyProtocolPresent(params);
+        verifyBundlesEqual(params, fromBundle);
+
+        params =
+                new FiraRangingReconfigureParams.Builder()
+                        .setBlockStrideLength(blockStrideLength)
+                        .setRangeDataNtfConfig(rangeDataNtfConfig)
+                        .setRangeDataProximityNear(rangeDataProximityNear)
+                        .setRangeDataProximityFar(rangeDataProximityFar)
+                        .build();
+        assertEquals((int) params.getBlockStrideLength(), blockStrideLength);
+        assertEquals((int) params.getRangeDataNtfConfig(), rangeDataNtfConfig);
+        assertEquals((int) params.getRangeDataProximityNear(), rangeDataProximityNear);
+        assertEquals((int) params.getRangeDataProximityFar(), rangeDataProximityFar);
+        fromBundle = FiraRangingReconfigureParams.fromBundle(params.toBundle());
+        assertEquals((int) fromBundle.getBlockStrideLength(), blockStrideLength);
+        assertEquals((int) fromBundle.getRangeDataNtfConfig(), rangeDataNtfConfig);
+        assertEquals((int) fromBundle.getRangeDataProximityNear(), rangeDataProximityNear);
+        assertEquals((int) fromBundle.getRangeDataProximityFar(), rangeDataProximityFar);
+
+        verifyProtocolPresent(params);
+        verifyBundlesEqual(params, fromBundle);
+    }
+
+    @Test
+    public void testControleeParams() {
+        UwbAddress uwbAddress1 = UwbAddress.fromBytes(new byte[] {1, 2});
+        UwbAddress uwbAddress2 = UwbAddress.fromBytes(new byte[] {4, 5});
+        UwbAddress[] addressList = new UwbAddress[] {uwbAddress1, uwbAddress2};
+        int[] subSessionIdList = new int[] {3, 4};
+        FiraControleeParams params =
+                new FiraControleeParams.Builder()
+                        .setAddressList(addressList)
+                        .setSubSessionIdList(subSessionIdList)
+                        .build();
+
+        assertArrayEquals(params.getAddressList(), addressList);
+        assertArrayEquals(params.getSubSessionIdList(), subSessionIdList);
+        FiraControleeParams fromBundle =
+                FiraControleeParams.fromBundle(params.toBundle());
+        assertArrayEquals(fromBundle.getAddressList(), addressList);
+        assertArrayEquals(fromBundle.getSubSessionIdList(), subSessionIdList);
+        verifyProtocolPresent(params);
+        verifyBundlesEqual(params, fromBundle);
+    }
+
+    @Test
+    public void testStatusCode() {
+        int statusCode = STATUS_CODE_ERROR_ADDRESS_ALREADY_PRESENT;
+        FiraStatusCode params = new FiraStatusCode.Builder().setStatusCode(statusCode).build();
+        assertEquals(params.getStatusCode(), statusCode);
+        assertTrue(FiraStatusCode.isBundleValid(params.toBundle()));
+        FiraStatusCode fromBundle = FiraStatusCode.fromBundle(params.toBundle());
+        assertEquals(fromBundle.getStatusCode(), statusCode);
+
+        verifyProtocolPresent(params);
+        verifyBundlesEqual(params, fromBundle);
+    }
+
+    @Test
+    public void testMulticastListUpdateStatusCode() {
+        int statusCode = MULTICAST_LIST_UPDATE_STATUS_ERROR_MULTICAST_LIST_FULL;
+        FiraMulticastListUpdateStatusCode params =
+                new FiraMulticastListUpdateStatusCode.Builder().setStatusCode(statusCode).build();
+        assertEquals(params.getStatusCode(), statusCode);
+        assertTrue(FiraMulticastListUpdateStatusCode.isBundleValid(params.toBundle()));
+
+        FiraMulticastListUpdateStatusCode fromBundle =
+                FiraMulticastListUpdateStatusCode.fromBundle(params.toBundle());
+        assertEquals(fromBundle.getStatusCode(), statusCode);
+
+        verifyProtocolPresent(params);
+        verifyBundlesEqual(params, fromBundle);
+    }
+
+    @Test
+    public void testStateChangeReasonCode() {
+        int reasonCode = STATE_CHANGE_REASON_CODE_ERROR_INVALID_RANGING_INTERVAL;
+        FiraStateChangeReasonCode params =
+                new FiraStateChangeReasonCode.Builder().setReasonCode(reasonCode).build();
+        assertEquals(reasonCode, params.getReasonCode());
+        assertTrue(FiraStateChangeReasonCode.isBundleValid(params.toBundle()));
+
+        FiraStateChangeReasonCode fromBundle =
+                FiraStateChangeReasonCode.fromBundle(params.toBundle());
+        assertEquals(reasonCode, fromBundle.getReasonCode());
+
+        verifyProtocolPresent(params);
+        verifyBundlesEqual(params, fromBundle);
+    }
+
+    private void verifyProtocolPresent(Params params) {
+        assertTrue(Params.isProtocol(params.toBundle(), FiraParams.PROTOCOL_NAME));
+    }
+
+    private void verifyBundlesEqual(Params params, Params fromBundle) {
+        PersistableBundle.kindofEquals(params.toBundle(), fromBundle.toBundle());
+    }
+
+    @Test
+    public void testSpecificationParams() {
+        FiraProtocolVersion minPhyVersionSupported = new FiraProtocolVersion(1, 0);
+        FiraProtocolVersion maxPhyVersionSupported = new FiraProtocolVersion(2, 0);
+        FiraProtocolVersion minMacVersionSupported = new FiraProtocolVersion(1, 2);
+        FiraProtocolVersion maxMacVersionSupported = new FiraProtocolVersion(1, 2);
+        List<Integer> supportedChannels = List.of(5, 6, 8, 9);
+        EnumSet<FiraParams.AoaCapabilityFlag> aoaCapabilities =
+                EnumSet.of(FiraParams.AoaCapabilityFlag.HAS_ELEVATION_SUPPORT);
+
+        EnumSet<FiraParams.DeviceRoleCapabilityFlag> deviceRoleCapabilities =
+                EnumSet.allOf(FiraParams.DeviceRoleCapabilityFlag.class);
+        boolean hasBlockStridingSupport = true;
+        boolean hasNonDeferredModeSupport = true;
+        boolean hasInitiationTimeSupport = true;
+        EnumSet<FiraParams.MultiNodeCapabilityFlag> multiNodeCapabilities =
+                EnumSet.allOf(FiraParams.MultiNodeCapabilityFlag.class);
+        EnumSet<FiraParams.PrfCapabilityFlag> prfCapabilities =
+                EnumSet.allOf(FiraParams.PrfCapabilityFlag.class);
+        EnumSet<FiraParams.RangingRoundCapabilityFlag> rangingRoundCapabilities =
+                EnumSet.allOf(FiraParams.RangingRoundCapabilityFlag.class);
+        EnumSet<FiraParams.RframeCapabilityFlag> rframeCapabilities =
+                EnumSet.allOf(FiraParams.RframeCapabilityFlag.class);
+        EnumSet<FiraParams.StsCapabilityFlag> stsCapabilities =
+                EnumSet.allOf(FiraParams.StsCapabilityFlag.class);
+        EnumSet<FiraParams.PsduDataRateCapabilityFlag> psduDataRateCapabilities =
+                EnumSet.allOf(FiraParams.PsduDataRateCapabilityFlag.class);
+        EnumSet<FiraParams.BprfParameterSetCapabilityFlag> bprfCapabilities =
+                EnumSet.allOf(FiraParams.BprfParameterSetCapabilityFlag.class);
+        EnumSet<FiraParams.HprfParameterSetCapabilityFlag> hprfCapabilities =
+                EnumSet.allOf(FiraParams.HprfParameterSetCapabilityFlag.class);
+
+        FiraSpecificationParams params =
+                new FiraSpecificationParams.Builder()
+                        .setMinPhyVersionSupported(minPhyVersionSupported)
+                        .setMaxPhyVersionSupported(maxPhyVersionSupported)
+                        .setMinMacVersionSupported(minMacVersionSupported)
+                        .setMaxMacVersionSupported(maxMacVersionSupported)
+                        .setSupportedChannels(supportedChannels)
+                        .setAoaCapabilities(aoaCapabilities)
+                        .setDeviceRoleCapabilities(deviceRoleCapabilities)
+                        .hasBlockStridingSupport(hasBlockStridingSupport)
+                        .hasNonDeferredModeSupport(hasNonDeferredModeSupport)
+                        .hasInitiationTimeSupport(hasInitiationTimeSupport)
+                        .setMultiNodeCapabilities(multiNodeCapabilities)
+                        .setPrfCapabilities(prfCapabilities)
+                        .setRangingRoundCapabilities(rangingRoundCapabilities)
+                        .setRframeCapabilities(rframeCapabilities)
+                        .setStsCapabilities(stsCapabilities)
+                        .setPsduDataRateCapabilities(psduDataRateCapabilities)
+                        .setBprfParameterSetCapabilities(bprfCapabilities)
+                        .setHprfParameterSetCapabilities(hprfCapabilities)
+                        .build();
+        assertEquals(minPhyVersionSupported, params.getMinPhyVersionSupported());
+        assertEquals(maxPhyVersionSupported, params.getMaxPhyVersionSupported());
+        assertEquals(minMacVersionSupported, params.getMinMacVersionSupported());
+        assertEquals(maxMacVersionSupported, params.getMaxMacVersionSupported());
+        assertEquals(supportedChannels, params.getSupportedChannels());
+        assertEquals(aoaCapabilities, params.getAoaCapabilities());
+        assertEquals(deviceRoleCapabilities, params.getDeviceRoleCapabilities());
+        assertEquals(hasBlockStridingSupport, params.hasBlockStridingSupport());
+        assertEquals(hasNonDeferredModeSupport, params.hasNonDeferredModeSupport());
+        assertEquals(hasInitiationTimeSupport, params.hasInitiationTimeSupport());
+        assertEquals(multiNodeCapabilities, params.getMultiNodeCapabilities());
+        assertEquals(prfCapabilities, params.getPrfCapabilities());
+        assertEquals(rangingRoundCapabilities, params.getRangingRoundCapabilities());
+        assertEquals(rframeCapabilities, params.getRframeCapabilities());
+        assertEquals(stsCapabilities, params.getStsCapabilities());
+        assertEquals(psduDataRateCapabilities, params.getPsduDataRateCapabilities());
+        assertEquals(bprfCapabilities, params.getBprfParameterSetCapabilities());
+        assertEquals(hprfCapabilities, params.getHprfParameterSetCapabilities());
+
+        FiraSpecificationParams fromBundle = FiraSpecificationParams.fromBundle(params.toBundle());
+        assertEquals(minPhyVersionSupported, fromBundle.getMinPhyVersionSupported());
+        assertEquals(maxPhyVersionSupported, fromBundle.getMaxPhyVersionSupported());
+        assertEquals(minMacVersionSupported, fromBundle.getMinMacVersionSupported());
+        assertEquals(maxMacVersionSupported, fromBundle.getMaxMacVersionSupported());
+        assertEquals(supportedChannels, fromBundle.getSupportedChannels());
+        assertEquals(aoaCapabilities, fromBundle.getAoaCapabilities());
+        assertEquals(deviceRoleCapabilities, fromBundle.getDeviceRoleCapabilities());
+        assertEquals(hasBlockStridingSupport, fromBundle.hasBlockStridingSupport());
+        assertEquals(hasNonDeferredModeSupport, fromBundle.hasNonDeferredModeSupport());
+        assertEquals(hasInitiationTimeSupport, params.hasInitiationTimeSupport());
+        assertEquals(multiNodeCapabilities, fromBundle.getMultiNodeCapabilities());
+        assertEquals(prfCapabilities, fromBundle.getPrfCapabilities());
+        assertEquals(rangingRoundCapabilities, fromBundle.getRangingRoundCapabilities());
+        assertEquals(rframeCapabilities, fromBundle.getRframeCapabilities());
+        assertEquals(stsCapabilities, fromBundle.getStsCapabilities());
+        assertEquals(psduDataRateCapabilities, fromBundle.getPsduDataRateCapabilities());
+        assertEquals(bprfCapabilities, fromBundle.getBprfParameterSetCapabilities());
+        assertEquals(hprfCapabilities, fromBundle.getHprfParameterSetCapabilities());
+        verifyProtocolPresent(params);
+        verifyBundlesEqual(params, fromBundle);
+    }
+}
diff --git a/service/support_lib/test/GenericTests.java b/service/support_lib/test/GenericTests.java
new file mode 100644
index 0000000..f92b43f
--- /dev/null
+++ b/service/support_lib/test/GenericTests.java
@@ -0,0 +1,205 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.ccc.CccProtocolVersion;
+import com.google.uwb.support.ccc.CccPulseShapeCombo;
+import com.google.uwb.support.ccc.CccSpecificationParams;
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraProtocolVersion;
+import com.google.uwb.support.fira.FiraSpecificationParams;
+import com.google.uwb.support.generic.GenericSpecificationParams;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.EnumSet;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class GenericTests {
+
+    @Test
+    public void testSpecificationParams() {
+        FiraProtocolVersion minPhyVersionSupported = new FiraProtocolVersion(1, 0);
+        FiraProtocolVersion maxPhyVersionSupported = new FiraProtocolVersion(2, 0);
+        FiraProtocolVersion minMacVersionSupported = new FiraProtocolVersion(1, 2);
+        FiraProtocolVersion maxMacVersionSupported = new FiraProtocolVersion(1, 2);
+        List<Integer> supportedChannels = List.of(5, 6, 8, 9);
+        EnumSet<FiraParams.AoaCapabilityFlag> aoaCapabilities =
+                EnumSet.of(FiraParams.AoaCapabilityFlag.HAS_ELEVATION_SUPPORT);
+
+        EnumSet<FiraParams.DeviceRoleCapabilityFlag> deviceRoleCapabilities =
+                EnumSet.allOf(FiraParams.DeviceRoleCapabilityFlag.class);
+        boolean hasBlockStridingSupport = true;
+        boolean hasNonDeferredModeSupport = true;
+        boolean hasInitiationTimeSupport = true;
+        EnumSet<FiraParams.MultiNodeCapabilityFlag> multiNodeCapabilities =
+                EnumSet.allOf(FiraParams.MultiNodeCapabilityFlag.class);
+        EnumSet<FiraParams.PrfCapabilityFlag> prfCapabilities =
+                EnumSet.allOf(FiraParams.PrfCapabilityFlag.class);
+        EnumSet<FiraParams.RangingRoundCapabilityFlag> rangingRoundCapabilities =
+                EnumSet.allOf(FiraParams.RangingRoundCapabilityFlag.class);
+        EnumSet<FiraParams.RframeCapabilityFlag> rframeCapabilities =
+                EnumSet.allOf(FiraParams.RframeCapabilityFlag.class);
+        EnumSet<FiraParams.StsCapabilityFlag> stsCapabilities =
+                EnumSet.allOf(FiraParams.StsCapabilityFlag.class);
+        EnumSet<FiraParams.PsduDataRateCapabilityFlag> psduDataRateCapabilities =
+                EnumSet.allOf(FiraParams.PsduDataRateCapabilityFlag.class);
+        EnumSet<FiraParams.BprfParameterSetCapabilityFlag> bprfCapabilities =
+                EnumSet.allOf(FiraParams.BprfParameterSetCapabilityFlag.class);
+        EnumSet<FiraParams.HprfParameterSetCapabilityFlag> hprfCapabilities =
+                EnumSet.allOf(FiraParams.HprfParameterSetCapabilityFlag.class);
+        FiraSpecificationParams firaSpecificationParams =
+                new FiraSpecificationParams.Builder()
+                        .setMinPhyVersionSupported(minPhyVersionSupported)
+                        .setMaxPhyVersionSupported(maxPhyVersionSupported)
+                        .setMinMacVersionSupported(minMacVersionSupported)
+                        .setMaxMacVersionSupported(maxMacVersionSupported)
+                        .setSupportedChannels(supportedChannels)
+                        .setAoaCapabilities(aoaCapabilities)
+                        .setDeviceRoleCapabilities(deviceRoleCapabilities)
+                        .hasBlockStridingSupport(hasBlockStridingSupport)
+                        .hasNonDeferredModeSupport(hasNonDeferredModeSupport)
+                        .hasInitiationTimeSupport(hasInitiationTimeSupport)
+                        .setMultiNodeCapabilities(multiNodeCapabilities)
+                        .setPrfCapabilities(prfCapabilities)
+                        .setRangingRoundCapabilities(rangingRoundCapabilities)
+                        .setRframeCapabilities(rframeCapabilities)
+                        .setStsCapabilities(stsCapabilities)
+                        .setPsduDataRateCapabilities(psduDataRateCapabilities)
+                        .setBprfParameterSetCapabilities(bprfCapabilities)
+                        .setHprfParameterSetCapabilities(hprfCapabilities)
+                        .build();
+
+        CccProtocolVersion[] protocolVersions =
+                new CccProtocolVersion[] {
+                        new CccProtocolVersion(1, 0),
+                        new CccProtocolVersion(2, 0),
+                        new CccProtocolVersion(2, 1)
+                };
+
+        Integer[] uwbConfigs = new Integer[] {CccParams.UWB_CONFIG_0, CccParams.UWB_CONFIG_1};
+        CccPulseShapeCombo[] pulseShapeCombos =
+                new CccPulseShapeCombo[] {
+                        new CccPulseShapeCombo(
+                                CccParams.PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE,
+                                CccParams.PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE),
+                        new CccPulseShapeCombo(
+                                CccParams.PULSE_SHAPE_PRECURSOR_FREE,
+                                CccParams.PULSE_SHAPE_PRECURSOR_FREE),
+                        new CccPulseShapeCombo(
+                                CccParams.PULSE_SHAPE_PRECURSOR_FREE_SPECIAL,
+                                CccParams.PULSE_SHAPE_PRECURSOR_FREE_SPECIAL)
+                };
+        int ranMultiplier = 200;
+        Integer[] chapsPerSlots =
+                new Integer[] {CccParams.CHAPS_PER_SLOT_4, CccParams.CHAPS_PER_SLOT_12};
+        Integer[] syncCodes =
+                new Integer[] {10, 23};
+        Integer[] channels = new Integer[] {CccParams.UWB_CHANNEL_5, CccParams.UWB_CHANNEL_9};
+        Integer[] hoppingConfigModes =
+                new Integer[] {
+                        CccParams.HOPPING_CONFIG_MODE_ADAPTIVE,
+                        CccParams.HOPPING_CONFIG_MODE_CONTINUOUS
+                };
+        Integer[] hoppingSequences =
+                new Integer[] {CccParams.HOPPING_SEQUENCE_AES, CccParams.HOPPING_SEQUENCE_DEFAULT};
+        CccSpecificationParams.Builder paramsBuilder = new CccSpecificationParams.Builder();
+        for (CccProtocolVersion p : protocolVersions) {
+            paramsBuilder.addProtocolVersion(p);
+        }
+        for (int uwbConfig : uwbConfigs) {
+            paramsBuilder.addUwbConfig(uwbConfig);
+        }
+        for (CccPulseShapeCombo pulseShapeCombo : pulseShapeCombos) {
+            paramsBuilder.addPulseShapeCombo(pulseShapeCombo);
+        }
+        paramsBuilder.setRanMultiplier(ranMultiplier);
+        for (int chapsPerSlot : chapsPerSlots) {
+            paramsBuilder.addChapsPerSlot(chapsPerSlot);
+        }
+        for (int syncCode : syncCodes) {
+            paramsBuilder.addSyncCode(syncCode);
+        }
+        for (int channel : channels) {
+            paramsBuilder.addChannel(channel);
+        }
+        for (int hoppingConfigMode : hoppingConfigModes) {
+            paramsBuilder.addHoppingConfigMode(hoppingConfigMode);
+        }
+        for (int hoppingSequence : hoppingSequences) {
+            paramsBuilder.addHoppingSequence(hoppingSequence);
+        }
+        CccSpecificationParams cccSpecificationParams = paramsBuilder.build();
+
+        boolean hasPowerStatsSupport = true;
+        GenericSpecificationParams genericSpecificationParams =
+                new GenericSpecificationParams.Builder()
+                        .setFiraSpecificationParams(firaSpecificationParams)
+                        .setCccSpecificationParams(cccSpecificationParams)
+                        .hasPowerStatsSupport(hasPowerStatsSupport)
+                        .build();
+        firaSpecificationParams = genericSpecificationParams.getFiraSpecificationParams();
+        cccSpecificationParams = genericSpecificationParams.getCccSpecificationParams();
+
+        assertEquals(minPhyVersionSupported, firaSpecificationParams.getMinPhyVersionSupported());
+        assertEquals(maxPhyVersionSupported, firaSpecificationParams.getMaxPhyVersionSupported());
+        assertEquals(minMacVersionSupported, firaSpecificationParams.getMinMacVersionSupported());
+        assertEquals(maxMacVersionSupported, firaSpecificationParams.getMaxMacVersionSupported());
+        assertEquals(supportedChannels, firaSpecificationParams.getSupportedChannels());
+        assertEquals(aoaCapabilities, firaSpecificationParams.getAoaCapabilities());
+        assertEquals(deviceRoleCapabilities, firaSpecificationParams.getDeviceRoleCapabilities());
+        assertEquals(hasBlockStridingSupport, firaSpecificationParams.hasBlockStridingSupport());
+        assertEquals(hasNonDeferredModeSupport,
+                firaSpecificationParams.hasNonDeferredModeSupport());
+        assertEquals(hasInitiationTimeSupport,
+                firaSpecificationParams.hasInitiationTimeSupport());
+        assertEquals(multiNodeCapabilities, firaSpecificationParams.getMultiNodeCapabilities());
+        assertEquals(prfCapabilities, firaSpecificationParams.getPrfCapabilities());
+        assertEquals(rangingRoundCapabilities,
+                firaSpecificationParams.getRangingRoundCapabilities());
+        assertEquals(rframeCapabilities, firaSpecificationParams.getRframeCapabilities());
+        assertEquals(stsCapabilities, firaSpecificationParams.getStsCapabilities());
+        assertEquals(psduDataRateCapabilities,
+                firaSpecificationParams.getPsduDataRateCapabilities());
+        assertEquals(bprfCapabilities, firaSpecificationParams.getBprfParameterSetCapabilities());
+        assertEquals(hprfCapabilities, firaSpecificationParams.getHprfParameterSetCapabilities());
+
+        assertArrayEquals(cccSpecificationParams.getProtocolVersions().toArray(), protocolVersions);
+        assertArrayEquals(cccSpecificationParams.getUwbConfigs().toArray(), uwbConfigs);
+        assertArrayEquals(cccSpecificationParams.getPulseShapeCombos().toArray(), pulseShapeCombos);
+        assertEquals(cccSpecificationParams.getRanMultiplier(), ranMultiplier);
+        assertArrayEquals(cccSpecificationParams.getChapsPerSlot().toArray(), chapsPerSlots);
+        assertArrayEquals(cccSpecificationParams.getSyncCodes().toArray(), syncCodes);
+        assertArrayEquals(cccSpecificationParams.getChannels().toArray(), channels);
+        assertArrayEquals(cccSpecificationParams.getHoppingConfigModes().toArray(),
+                hoppingConfigModes);
+        assertArrayEquals(cccSpecificationParams.getHoppingSequences().toArray(),
+                hoppingSequences);
+
+        assertEquals(hasPowerStatsSupport, genericSpecificationParams.hasPowerStatsSupport());
+    }
+}
diff --git a/service/support_lib/test/MultichipTests.java b/service/support_lib/test/MultichipTests.java
new file mode 100644
index 0000000..a6d47d9
--- /dev/null
+++ b/service/support_lib/test/MultichipTests.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.PersistableBundle;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.uwb.support.multichip.ChipInfoParams;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MultichipTests {
+
+    @Test
+    public void testChipInfoParams() {
+        String chipId = "testChipId";
+        double positionX = 1.0;
+        double positionY = 2.0;
+        double positionZ = 3.0;
+
+        // Use Builder to build chipInfoParams
+        ChipInfoParams chipInfoParams = ChipInfoParams.createBuilder().setChipId(chipId)
+                .setPositionX(positionX).setPositionY(positionY).setPositionZ(positionZ).build();
+
+        assertEquals(chipInfoParams.getChipId(), chipId);
+        assertEquals(chipInfoParams.getPositionX(), positionX, 0);
+        assertEquals(chipInfoParams.getPositionY(), positionY, 0);
+        assertEquals(chipInfoParams.getPositionZ(), positionZ, 0);
+
+        // Convert to and from PersistableBundle
+        PersistableBundle bundle = chipInfoParams.toBundle();
+        ChipInfoParams fromBundle = ChipInfoParams.fromBundle(bundle);
+
+        assertEquals(fromBundle.getChipId(), chipId);
+        assertEquals(fromBundle.getPositionX(), positionX, 0);
+        assertEquals(fromBundle.getPositionY(), positionY, 0);
+        assertEquals(fromBundle.getPositionZ(), positionZ, 0);
+    }
+
+    @Test
+    public void testChipInfoParamsDefaults() {
+        ChipInfoParams chipInfoParams = ChipInfoParams.createBuilder().build();
+
+        assertEquals(chipInfoParams.getChipId(), "UNKNOWN_CHIP_ID");
+        assertEquals(chipInfoParams.getPositionX(), 0.0, 0);
+        assertEquals(chipInfoParams.getPositionY(), 0.0, 0);
+        assertEquals(chipInfoParams.getPositionZ(), 0.0, 0);
+    }
+}
diff --git a/service/support_lib/test/RequiredParamTests.java b/service/support_lib/test/RequiredParamTests.java
new file mode 100644
index 0000000..cfa2016
--- /dev/null
+++ b/service/support_lib/test/RequiredParamTests.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+package com.google.uwb.support;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.uwb.support.base.RequiredParam;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RequiredParamTests {
+    @Test
+    public void testSetAndGet() {
+        RequiredParam<Integer> requiredParam = new RequiredParam<>();
+        try {
+            requiredParam.get();
+            fail("Should not be able to get parameter yet");
+        } catch (IllegalStateException e) {
+            // Expected behavior
+        }
+
+        Integer val = 1;
+        requiredParam.set(val);
+        assertEquals(val, requiredParam.get());
+    }
+}
diff --git a/service/tests/Android.bp b/service/tests/Android.bp
new file mode 100644
index 0000000..7eb8edd
--- /dev/null
+++ b/service/tests/Android.bp
@@ -0,0 +1,76 @@
+// 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.
+
+// Make test APK
+// ============================================================
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "ServiceUwbTests",
+
+    srcs: [
+        ":framework-uwb-test-util-srcs",
+        "**/*.java"
+    ],
+
+    dxflags: ["--multi-dex"],
+
+    java_version: "1.9",
+
+    static_libs: [
+        "androidx.test.rules",
+        "collector-device-lib",
+        "hamcrest-library",
+        "mockito-target-extended-minus-junit4",
+        "platform-test-annotations",
+        "frameworks-base-testutils",
+        "truth-prebuilt",
+
+        // Statically link service-uwb-pre-jarjar since we want to test the working copy of
+        // service-uwb, not the on-device copy.
+        // Use pre-jarjar version so that we can reference symbols before they are renamed.
+        // Then, the jarjar_rules here will perform the rename for the entire APK
+        // i.e. service-uwb + test code
+        "service-uwb-pre-jarjar",
+    ],
+
+    jarjar_rules: ":uwb-jarjar-rules",
+
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+        "android.test.mock",
+        "framework-annotations-lib",
+        "framework-uwb-pre-jarjar",
+        "ServiceUwbResources",
+        "framework-statsd.stubs.module_lib",
+        "framework-wifi.stubs.module_lib"
+    ],
+
+    jni_libs: [
+        // these are needed for Extended Mockito
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+    compile_multilib: "both",
+
+    min_sdk_version: "Tiramisu",
+
+    test_suites: [
+        "general-tests",
+        "mts-uwb",
+    ],
+}
diff --git a/service/tests/AndroidManifest.xml b/service/tests/AndroidManifest.xml
new file mode 100644
index 0000000..92ac02e
--- /dev/null
+++ b/service/tests/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.server.uwb.test">
+
+    <application android:debuggable="true"
+         android:largeHeap="true">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:label="UwbTestDummyLabel"
+             android:name="UwbTestDummyName"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+
+    <instrumentation android:name="com.android.server.uwb.CustomTestRunner"
+         android:targetPackage="com.android.server.uwb.test"
+         android:label="Frameworks Uwb Tests">
+    </instrumentation>
+
+    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+
+</manifest>
diff --git a/service/tests/AndroidTest.xml b/service/tests/AndroidTest.xml
new file mode 100644
index 0000000..ddfbe6c
--- /dev/null
+++ b/service/tests/AndroidTest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs Frameworks Uwb Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="test-file-name" value="ServiceUwbTests.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-tag" value="ServiceUwbTests" />
+    <option name="config-descriptor:metadata" key="mainline-param"
+            value="com.google.android.uwb.apex" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.server.uwb.test" />
+        <option name="runner" value="com.android.server.uwb.CustomTestRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+
+    <!-- Only run FrameworksUwbTests in MTS if the Uwb Mainline module is installed. -->
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.uwb" />
+    </object>
+</configuration>
diff --git a/service/tests/assets/noPositionConfig.xml b/service/tests/assets/noPositionConfig.xml
new file mode 100644
index 0000000..715d554
--- /dev/null
+++ b/service/tests/assets/noPositionConfig.xml
@@ -0,0 +1,8 @@
+<uwbChipConfig>
+    <defaultChipId>chipIdString</defaultChipId>
+    <chipGroup>
+        <chip>
+            <id>chipIdString</id>
+        </chip>
+    </chipGroup>
+</uwbChipConfig>
\ No newline at end of file
diff --git a/service/tests/assets/singleChipConfig.xml b/service/tests/assets/singleChipConfig.xml
new file mode 100644
index 0000000..af28484
--- /dev/null
+++ b/service/tests/assets/singleChipConfig.xml
@@ -0,0 +1,13 @@
+<uwbChipConfig>
+    <defaultChipId>chipIdString</defaultChipId>
+    <chipGroup>
+        <chip>
+            <id>chipIdString</id>
+            <position>
+                <x>1.0</x>
+                <y>2.0</y>
+                <z>3.0</z>
+            </position>
+        </chip>
+    </chipGroup>
+</uwbChipConfig>
\ No newline at end of file
diff --git a/service/tests/assets/twoChipConfig.xml b/service/tests/assets/twoChipConfig.xml
new file mode 100644
index 0000000..94cd6ff
--- /dev/null
+++ b/service/tests/assets/twoChipConfig.xml
@@ -0,0 +1,21 @@
+<uwbChipConfig>
+    <defaultChipId>chipIdString</defaultChipId>
+    <chipGroup>
+        <chip>
+            <id>chipIdString1</id>
+            <position>
+                <x>1.0</x>
+                <y>2.0</y>
+                <z>3.0</z>
+            </position>
+        </chip>
+        <chip>
+            <id>chipIdString2</id>
+            <position>
+                <x>4.0</x>
+                <y>5.0</y>
+                <z>6.0</z>
+            </position>
+        </chip>
+    </chipGroup>
+</uwbChipConfig>
\ No newline at end of file
diff --git a/service/tests/src/com/android/server/uwb/BinderUtil.java b/service/tests/src/com/android/server/uwb/BinderUtil.java
new file mode 100644
index 0000000..5b9b5e9
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/BinderUtil.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import android.os.Binder;
+
+/**
+ * Utilities for faking the calling uid in Binder.
+ */
+public class BinderUtil {
+    /**
+     * Fake the calling uid in Binder.
+     * @param uid the calling uid that Binder should return from now on
+     */
+    public static void setUid(int uid) {
+        Binder.restoreCallingIdentity((((long) uid) << 32) | Binder.getCallingPid());
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/CustomTestRunner.java b/service/tests/src/com/android/server/uwb/CustomTestRunner.java
new file mode 100644
index 0000000..abf1082
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/CustomTestRunner.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.test.runner.AndroidJUnitRunner;
+
+import java.lang.reflect.Method;
+
+public class CustomTestRunner extends AndroidJUnitRunner {
+    @Override
+    public void onCreate(Bundle arguments) {
+        // Override the default TerribleFailureHandler, as that handler might terminate
+        // the process (if we're on an eng build).
+        // Use reflection since we are compiling the tests against SDK and |setWtfHandler| is @hide.
+        try {
+            Class<Log> clazz = Log.class;
+            Method method = clazz.getMethod("setWtfHandler", Log.TerribleFailureHandler.class);
+            Log.TerribleFailureHandler handler = (tag, what, system) -> Log.e(tag, "WTF", what);
+            method.invoke(null, handler);
+        } catch (Exception e) {
+            Log.e("CustomTestRunner", "Failed to set wtf handler", e);
+        }
+        super.onCreate(arguments);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/DeviceConfigFacadeTest.java b/service/tests/src/com/android/server/uwb/DeviceConfigFacadeTest.java
new file mode 100644
index 0000000..59025a5
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/DeviceConfigFacadeTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+import android.app.test.MockAnswerUtil;
+import android.os.Handler;
+import android.os.test.TestLooper;
+import android.provider.DeviceConfig;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+
+public class DeviceConfigFacadeTest {
+    @Mock UwbInjector mUwbInjector;
+
+    final ArgumentCaptor<DeviceConfig.OnPropertiesChangedListener>
+            mOnPropertiesChangedListenerCaptor =
+            ArgumentCaptor.forClass(DeviceConfig.OnPropertiesChangedListener.class);
+
+    private DeviceConfigFacade mDeviceConfigFacade;
+    private TestLooper mLooper = new TestLooper();
+    private MockitoSession mSession;
+
+    /**
+     * Setup the mocks and an instance of WifiConfigManager before each test.
+     */
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        // static mocking
+        mSession = ExtendedMockito.mockitoSession()
+                .mockStatic(DeviceConfig.class, withSettings().lenient())
+                .startMocking();
+        // Have DeviceConfig return the default value passed in.
+        when(DeviceConfig.getBoolean(anyString(), anyString(), anyBoolean()))
+                .then(new MockAnswerUtil.AnswerWithArguments() {
+                    public boolean answer(String namespace, String field, boolean def) {
+                        return def;
+                    }
+                });
+        when(DeviceConfig.getInt(anyString(), anyString(), anyInt()))
+                .then(new MockAnswerUtil.AnswerWithArguments() {
+                    public int answer(String namespace, String field, int def) {
+                        return def;
+                    }
+                });
+        when(DeviceConfig.getLong(anyString(), anyString(), anyLong()))
+                .then(new MockAnswerUtil.AnswerWithArguments() {
+                    public long answer(String namespace, String field, long def) {
+                        return def;
+                    }
+                });
+        when(DeviceConfig.getString(anyString(), anyString(), anyString()))
+                .then(new MockAnswerUtil.AnswerWithArguments() {
+                    public String answer(String namespace, String field, String def) {
+                        return def;
+                    }
+                });
+
+        mDeviceConfigFacade = new DeviceConfigFacade(new Handler(mLooper.getLooper()),
+                mUwbInjector);
+        verify(() -> DeviceConfig.addOnPropertiesChangedListener(anyString(), any(),
+                mOnPropertiesChangedListenerCaptor.capture()));
+    }
+
+    /**
+     * Called after each test
+     */
+    @After
+    public void cleanup() {
+        validateMockitoUsage();
+        mSession.finishMocking();
+    }
+
+    /**
+     * Verifies that default values are set correctly
+     */
+    @Test
+    public void testDefaultValue() throws Exception {
+        assertEquals(DeviceConfigFacade.DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS,
+                mDeviceConfigFacade.getRangingResultLogIntervalMs());
+        assertEquals(false, mDeviceConfigFacade.isDeviceErrorBugreportEnabled());
+        assertEquals(DeviceConfigFacade.DEFAULT_BUG_REPORT_MIN_INTERVAL_MS,
+                mDeviceConfigFacade.getBugReportMinIntervalMs());
+    }
+
+    /**
+     * Verifies that all fields are updated properly.
+     */
+    @Test
+    public void testFieldUpdates() throws Exception {
+        // Simulate updating the fields
+        when(DeviceConfig.getInt(anyString(), eq("ranging_result_log_interval_ms"),
+                anyInt())).thenReturn(4000);
+        when(DeviceConfig.getBoolean(anyString(), eq("device_error_bugreport_enabled"),
+                anyBoolean())).thenReturn(true);
+        when(DeviceConfig.getInt(anyString(), eq("bug_report_min_interval_ms"),
+                anyInt())).thenReturn(10 * 3600_000);
+
+        mOnPropertiesChangedListenerCaptor.getValue().onPropertiesChanged(null);
+
+        // Verifying fields are updated to the new values
+        assertEquals(4000, mDeviceConfigFacade.getRangingResultLogIntervalMs());
+        assertEquals(true, mDeviceConfigFacade.isDeviceErrorBugreportEnabled());
+        assertEquals(10 * 3600_000, mDeviceConfigFacade.getBugReportMinIntervalMs());
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/UwbConfigurationManagerTest.java b/service/tests/src/com/android/server/uwb/UwbConfigurationManagerTest.java
new file mode 100644
index 0000000..373b84c
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/UwbConfigurationManagerTest.java
@@ -0,0 +1,251 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import static com.google.uwb.support.fira.FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_INTERLEAVED;
+import static com.google.uwb.support.fira.FiraParams.AOA_TYPE_AZIMUTH_AND_ELEVATION;
+import static com.google.uwb.support.fira.FiraParams.BPRF_PHR_DATA_RATE_6M81;
+import static com.google.uwb.support.fira.FiraParams.MAC_ADDRESS_MODE_8_BYTES;
+import static com.google.uwb.support.fira.FiraParams.MAC_FCS_TYPE_CRC_32;
+import static com.google.uwb.support.fira.FiraParams.MEASUREMENT_REPORT_TYPE_INITIATOR_TO_RESPONDER;
+import static com.google.uwb.support.fira.FiraParams.MULTI_NODE_MODE_MANY_TO_MANY;
+import static com.google.uwb.support.fira.FiraParams.PREAMBLE_DURATION_T32_SYMBOLS;
+import static com.google.uwb.support.fira.FiraParams.PRF_MODE_HPRF;
+import static com.google.uwb.support.fira.FiraParams.PSDU_DATA_RATE_7M80;
+import static com.google.uwb.support.fira.FiraParams.RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY;
+import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_ROLE_INITIATOR;
+import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_TYPE_CONTROLEE;
+import static com.google.uwb.support.fira.FiraParams.RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE;
+import static com.google.uwb.support.fira.FiraParams.RFRAME_CONFIG_SP1;
+import static com.google.uwb.support.fira.FiraParams.SFD_ID_VALUE_3;
+import static com.google.uwb.support.fira.FiraParams.STS_CONFIG_DYNAMIC_FOR_CONTROLEE_INDIVIDUAL_KEY;
+import static com.google.uwb.support.fira.FiraParams.STS_LENGTH_128_SYMBOLS;
+import static com.google.uwb.support.fira.FiraParams.STS_SEGMENT_COUNT_VALUE_2;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.uwb.UwbAddress;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.data.UwbConfigStatusData;
+import com.android.server.uwb.data.UwbTlvData;
+import com.android.server.uwb.data.UwbUciConstants;
+import com.android.server.uwb.jni.NativeUwbManager;
+import com.android.server.uwb.proto.UwbStatsLog;
+
+import com.google.uwb.support.fira.FiraOpenSessionParams;
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraProtocolVersion;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for {@link UwbConfigurationManager}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class UwbConfigurationManagerTest {
+    @Mock
+    private NativeUwbManager mNativeUwbManager;
+    private UwbConfigurationManager mUwbConfigurationManager;
+    @Mock
+    private UwbSessionManager.UwbSession mUwbSession;
+    private FiraOpenSessionParams mFiraParams;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mUwbConfigurationManager = new UwbConfigurationManager(mNativeUwbManager);
+        mFiraParams = getFiraParams();
+
+        when(mUwbSession.getSessionId()).thenReturn(1);
+        when(mUwbSession.getProtocolName()).thenReturn(FiraParams.PROTOCOL_NAME);
+        when(mUwbSession.getProfileType()).thenReturn(
+                UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__FIRA);
+        when(mUwbSession.getParams()).thenReturn(mFiraParams);
+    }
+
+    @Test
+    public void testSetAppConfigurations() throws Exception {
+        byte[] cfgStatus = {0x01, UwbUciConstants.STATUS_CODE_OK};
+        UwbConfigStatusData appConfig = new UwbConfigStatusData(UwbUciConstants.STATUS_CODE_OK,
+                1, cfgStatus);
+        when(mNativeUwbManager.setAppConfigurations(anyInt(), anyInt(), anyInt(),
+                any(byte[].class))).thenReturn(appConfig);
+
+        int status = mUwbConfigurationManager
+                .setAppConfigurations(mUwbSession.getSessionId(), mFiraParams);
+
+        verify(mNativeUwbManager).setAppConfigurations(anyInt(), anyInt(), anyInt(),
+                any(byte[].class));
+        assertEquals(UwbUciConstants.STATUS_CODE_OK, status);
+    }
+
+    @Test
+    public void testGetAppConfigurations() throws Exception {
+        byte[] tlvs = {0x01, 0x02, 0x02, 0x03};
+        UwbTlvData getAppConfig = new UwbTlvData(UwbUciConstants.STATUS_CODE_OK, 1, tlvs);
+        when(mNativeUwbManager.getAppConfigurations(anyInt(), anyInt(), anyInt(),
+                any(byte[].class))).thenReturn(getAppConfig);
+
+        mUwbConfigurationManager.getAppConfigurations(mUwbSession.getSessionId(),
+                mFiraParams.getProtocolName(), new byte[0], FiraOpenSessionParams.class);
+
+        verify(mNativeUwbManager).getAppConfigurations(anyInt(), anyInt(), anyInt(),
+                any(byte[].class));
+    }
+
+    @Test
+    public void testGetCapsInfo() throws Exception {
+        byte[] tlvs = {0x01, 0x02, 0x02, 0x03};
+        UwbTlvData getAppConfig = new UwbTlvData(UwbUciConstants.STATUS_CODE_OK, 1, tlvs);
+        when(mNativeUwbManager.getCapsInfo()).thenReturn(getAppConfig);
+
+        mUwbConfigurationManager.getCapsInfo(mFiraParams.getProtocolName(),
+                FiraOpenSessionParams.class);
+
+        verify(mNativeUwbManager).getCapsInfo();
+    }
+
+    private FiraOpenSessionParams getFiraParams() {
+        FiraProtocolVersion protocolVersion = FiraParams.PROTOCOL_VERSION_1_1;
+        int sessionId = 10;
+        int deviceType = RANGING_DEVICE_TYPE_CONTROLEE;
+        int deviceRole = RANGING_DEVICE_ROLE_INITIATOR;
+        int rangingRoundUsage = RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE;
+        int multiNodeMode = MULTI_NODE_MODE_MANY_TO_MANY;
+        int addressMode = MAC_ADDRESS_MODE_8_BYTES;
+        UwbAddress deviceAddress = UwbAddress.fromBytes(new byte[] {1, 2, 3, 4, 5, 6, 7, 8});
+        UwbAddress destAddress1 = UwbAddress.fromBytes(new byte[] {1, 2, 3, 4, 5, 6, 7, 8});
+        UwbAddress destAddress2 =
+                UwbAddress.fromBytes(new byte[] {(byte) 0xFF, (byte) 0xFE, 3, 4, 5, 6, 7, 8});
+        List<UwbAddress> destAddressList = new ArrayList<>();
+        destAddressList.add(destAddress1);
+        destAddressList.add(destAddress2);
+        int initiationTimeMs = 100;
+        int slotDurationRstu = 2400;
+        int slotsPerRangingRound = 10;
+        int rangingIntervalMs = 100;
+        int blockStrideLength = 2;
+        int maxRangingRoundRetries = 3;
+        int sessionPriority = 100;
+        boolean hasResultReportPhase = true;
+        int measurementReportType = MEASUREMENT_REPORT_TYPE_INITIATOR_TO_RESPONDER;
+        int inBandTerminationAttemptCount = 8;
+        int channelNumber = 10;
+        int preambleCodeIndex = 12;
+        int rframeConfig = RFRAME_CONFIG_SP1;
+        int prfMode = PRF_MODE_HPRF;
+        int preambleDuration = PREAMBLE_DURATION_T32_SYMBOLS;
+        int sfdId = SFD_ID_VALUE_3;
+        int stsSegmentCount = STS_SEGMENT_COUNT_VALUE_2;
+        int stsLength = STS_LENGTH_128_SYMBOLS;
+        int psduDataRate = PSDU_DATA_RATE_7M80;
+        int bprfPhrDataRate = BPRF_PHR_DATA_RATE_6M81;
+        int fcsType = MAC_FCS_TYPE_CRC_32;
+        boolean isTxAdaptivePayloadPowerEnabled = true;
+        int stsConfig = STS_CONFIG_DYNAMIC_FOR_CONTROLEE_INDIVIDUAL_KEY;
+        int subSessionId = 24;
+        byte[] vendorId = new byte[] {(byte) 0xFE, (byte) 0xDC};
+        byte[] staticStsIV = new byte[] {(byte) 0xDF, (byte) 0xCE, (byte) 0xAB, 0x12, 0x34, 0x56};
+        boolean isKeyRotationEnabled = true;
+        int keyRotationRate = 15;
+        int aoaResultRequest = AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_INTERLEAVED;
+        int rangeDataNtfConfig = RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY;
+        int rangeDataNtfProximityNear = 50;
+        int rangeDataNtfProximityFar = 200;
+        boolean hasTimeOfFlightReport = true;
+        boolean hasAngleOfArrivalAzimuthReport = true;
+        boolean hasAngleOfArrivalElevationReport = true;
+        boolean hasAngleOfArrivalFigureOfMeritReport = true;
+        int aoaType = AOA_TYPE_AZIMUTH_AND_ELEVATION;
+        int numOfMsrmtFocusOnRange = 1;
+        int numOfMsrmtFocusOnAoaAzimuth = 2;
+        int numOfMsrmtFocusOnAoaElevation = 3;
+
+        FiraOpenSessionParams params =
+                new FiraOpenSessionParams.Builder()
+                        .setProtocolVersion(protocolVersion)
+                        .setSessionId(sessionId)
+                        .setDeviceType(deviceType)
+                        .setDeviceRole(deviceRole)
+                        .setRangingRoundUsage(rangingRoundUsage)
+                        .setMultiNodeMode(multiNodeMode)
+                        .setDeviceAddress(deviceAddress)
+                        .setDestAddressList(destAddressList)
+                        .setInitiationTimeMs(initiationTimeMs)
+                        .setSlotDurationRstu(slotDurationRstu)
+                        .setSlotsPerRangingRound(slotsPerRangingRound)
+                        .setRangingIntervalMs(rangingIntervalMs)
+                        .setBlockStrideLength(blockStrideLength)
+                        .setMaxRangingRoundRetries(maxRangingRoundRetries)
+                        .setSessionPriority(sessionPriority)
+                        .setMacAddressMode(addressMode)
+                        .setHasResultReportPhase(hasResultReportPhase)
+                        .setMeasurementReportType(measurementReportType)
+                        .setInBandTerminationAttemptCount(inBandTerminationAttemptCount)
+                        .setChannelNumber(channelNumber)
+                        .setPreambleCodeIndex(preambleCodeIndex)
+                        .setRframeConfig(rframeConfig)
+                        .setPrfMode(prfMode)
+                        .setPreambleDuration(preambleDuration)
+                        .setSfdId(sfdId)
+                        .setStsSegmentCount(stsSegmentCount)
+                        .setStsLength(stsLength)
+                        .setPsduDataRate(psduDataRate)
+                        .setBprfPhrDataRate(bprfPhrDataRate)
+                        .setFcsType(fcsType)
+                        .setIsTxAdaptivePayloadPowerEnabled(isTxAdaptivePayloadPowerEnabled)
+                        .setStsConfig(stsConfig)
+                        .setSubSessionId(subSessionId)
+                        .setVendorId(vendorId)
+                        .setStaticStsIV(staticStsIV)
+                        .setIsKeyRotationEnabled(isKeyRotationEnabled)
+                        .setKeyRotationRate(keyRotationRate)
+                        .setAoaResultRequest(aoaResultRequest)
+                        .setRangeDataNtfConfig(rangeDataNtfConfig)
+                        .setRangeDataNtfProximityNear(rangeDataNtfProximityNear)
+                        .setRangeDataNtfProximityFar(rangeDataNtfProximityFar)
+                        .setHasTimeOfFlightReport(hasTimeOfFlightReport)
+                        .setHasAngleOfArrivalAzimuthReport(hasAngleOfArrivalAzimuthReport)
+                        .setHasAngleOfArrivalElevationReport(hasAngleOfArrivalElevationReport)
+                        .setHasAngleOfArrivalFigureOfMeritReport(
+                                hasAngleOfArrivalFigureOfMeritReport)
+                        .setAoaType(aoaType)
+                        .setMeasurementFocusRatio(
+                                numOfMsrmtFocusOnRange,
+                                numOfMsrmtFocusOnAoaAzimuth,
+                                numOfMsrmtFocusOnAoaElevation)
+                        .build();
+        return params;
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/UwbCountryCodeTest.java b/service/tests/src/com/android/server/uwb/UwbCountryCodeTest.java
new file mode 100644
index 0000000..4bb5a84
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/UwbCountryCodeTest.java
@@ -0,0 +1,209 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import static org.mockito.Mockito.*;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback;
+import android.os.Handler;
+import android.os.test.TestLooper;
+import android.telephony.TelephonyManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.uwb.data.UwbUciConstants;
+import com.android.server.uwb.jni.NativeUwbManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Unit tests for {@link com.android.server.uwb.UwbCountryCode}.
+ */
+@SmallTest
+public class UwbCountryCodeTest {
+    private static final String TEST_COUNTRY_CODE = "US";
+    private static final String TEST_COUNTRY_CODE_OTHER = "JP";
+
+    @Mock Context mContext;
+    @Mock TelephonyManager mTelephonyManager;
+    @Mock WifiManager mWifiManager;
+    @Mock NativeUwbManager mNativeUwbManager;
+    @Mock UwbInjector mUwbInjector;
+    @Mock PackageManager mPackageManager;
+    private TestLooper mTestLooper;
+    private UwbCountryCode mUwbCountryCode;
+
+    @Captor
+    private ArgumentCaptor<BroadcastReceiver> mTelephonyCountryCodeReceiverCaptor;
+    @Captor
+    private ArgumentCaptor<ActiveCountryCodeChangedCallback> mWifiCountryCodeReceiverCaptor;
+
+    /**
+     * Setup test.
+     */
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mTestLooper = new TestLooper();
+
+        when(mContext.getSystemService(TelephonyManager.class))
+                .thenReturn(mTelephonyManager);
+        when(mContext.getSystemService(WifiManager.class))
+                .thenReturn(mWifiManager);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
+        when(mNativeUwbManager.setCountryCode(any())).thenReturn(
+                (byte) UwbUciConstants.STATUS_CODE_OK);
+        mUwbCountryCode = new UwbCountryCode(
+                mContext, mNativeUwbManager, new Handler(mTestLooper.getLooper()), mUwbInjector);
+    }
+
+    @Test
+    public void testSetDefaultCountryCodeWhenNoCountryCodeAvailable() {
+        mUwbCountryCode.initialize();
+        verify(mNativeUwbManager).setCountryCode(
+                UwbCountryCode.DEFAULT_COUNTRY_CODE.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testInitializeCountryCodeFromTelephony() {
+        when(mTelephonyManager.getNetworkCountryIso()).thenReturn(TEST_COUNTRY_CODE);
+        mUwbCountryCode.initialize();
+        verify(mNativeUwbManager).setCountryCode(
+                TEST_COUNTRY_CODE.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testInitializeCountryCodeFromTelephonyVerifyListener() {
+        UwbCountryCode.CountryCodeChangedListener listener = mock(
+                UwbCountryCode.CountryCodeChangedListener.class);
+        mUwbCountryCode.addListener(listener);
+        when(mTelephonyManager.getNetworkCountryIso()).thenReturn(TEST_COUNTRY_CODE);
+        mUwbCountryCode.initialize();
+        verify(mNativeUwbManager).setCountryCode(
+                TEST_COUNTRY_CODE.getBytes(StandardCharsets.UTF_8));
+        verify(listener).onCountryCodeChanged(TEST_COUNTRY_CODE);
+    }
+
+    @Test
+    public void testSetCountryCodeFromTelephony() {
+        when(mTelephonyManager.getNetworkCountryIso()).thenReturn(TEST_COUNTRY_CODE);
+        mUwbCountryCode.initialize();
+        clearInvocations(mNativeUwbManager);
+
+        mUwbCountryCode.setCountryCode(false);
+        // already set.
+        verify(mNativeUwbManager, never()).setCountryCode(
+                TEST_COUNTRY_CODE.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testSetCountryCodeWithForceUpdateFromTelephony() {
+        when(mTelephonyManager.getNetworkCountryIso()).thenReturn(TEST_COUNTRY_CODE);
+        mUwbCountryCode.initialize();
+        clearInvocations(mNativeUwbManager);
+
+        mUwbCountryCode.setCountryCode(true);
+        // set again
+        verify(mNativeUwbManager).setCountryCode(
+                TEST_COUNTRY_CODE.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testSetCountryCodeFromOemWhenTelephonyAndWifiNotAvailable() {
+        when(mUwbInjector.getOemDefaultCountryCode()).thenReturn(TEST_COUNTRY_CODE);
+        mUwbCountryCode.initialize();
+        clearInvocations(mNativeUwbManager);
+
+        mUwbCountryCode.setCountryCode(false);
+        // already set.
+        verify(mNativeUwbManager, never()).setCountryCode(
+                TEST_COUNTRY_CODE.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testChangeInTelephonyCountryCode() {
+        mUwbCountryCode.initialize();
+        verify(mContext).registerReceiver(
+                mTelephonyCountryCodeReceiverCaptor.capture(), any(), any(), any());
+        Intent intent = new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+                .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE);
+        mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mock(Context.class), intent);
+        verify(mNativeUwbManager).setCountryCode(
+                TEST_COUNTRY_CODE.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testChangeInWifiCountryCode() {
+        mUwbCountryCode.initialize();
+        verify(mWifiManager).registerActiveCountryCodeChangedCallback(
+                any(), mWifiCountryCodeReceiverCaptor.capture());
+        mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE);
+        verify(mNativeUwbManager).setCountryCode(
+                TEST_COUNTRY_CODE.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testChangeInTelephonyCountryCodeWhenWifiCountryCodeAvailable() {
+        mUwbCountryCode.initialize();
+        verify(mWifiManager).registerActiveCountryCodeChangedCallback(
+                any(), mWifiCountryCodeReceiverCaptor.capture());
+        mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE);
+        verify(mContext).registerReceiver(
+                mTelephonyCountryCodeReceiverCaptor.capture(), any(), any(), any());
+        verify(mNativeUwbManager).setCountryCode(
+                TEST_COUNTRY_CODE.getBytes(StandardCharsets.UTF_8));
+
+        Intent intent = new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+                .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_OTHER);
+        mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mock(Context.class), intent);
+        verify(mNativeUwbManager).setCountryCode(
+                TEST_COUNTRY_CODE_OTHER.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testForceOverrideCodeWhenTelephonyAndWifiAvailable() {
+        when(mTelephonyManager.getNetworkCountryIso()).thenReturn(TEST_COUNTRY_CODE);
+        mUwbCountryCode.initialize();
+
+        verify(mWifiManager).registerActiveCountryCodeChangedCallback(
+                any(), mWifiCountryCodeReceiverCaptor.capture());
+        mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE);
+        clearInvocations(mNativeUwbManager);
+
+        mUwbCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_OTHER);
+        verify(mNativeUwbManager).setCountryCode(
+                TEST_COUNTRY_CODE_OTHER.getBytes(StandardCharsets.UTF_8));
+        clearInvocations(mNativeUwbManager);
+
+        mUwbCountryCode.clearOverrideCountryCode();
+        verify(mNativeUwbManager).setCountryCode(
+                TEST_COUNTRY_CODE.getBytes(StandardCharsets.UTF_8));
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/UwbDiagnosticsTest.java b/service/tests/src/com/android/server/uwb/UwbDiagnosticsTest.java
new file mode 100644
index 0000000..51f785f
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/UwbDiagnosticsTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.BugreportManager;
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link com.android.server.uwb.UwbDiagnostics}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class UwbDiagnosticsTest {
+    @Mock SystemBuildProperties mBuildProperties;
+    @Mock Context mContext;
+    @Mock UwbInjector mUwbInjector;
+    @Mock DeviceConfigFacade mDeviceConfigFacade;
+    @Mock BugreportManager mBugreportManager;
+    UwbDiagnostics mUwbDiagnostics;
+
+    private static final int BUG_REPORT_MIN_INTERVAL_MS = 3600_000;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        when(mUwbInjector.getDeviceConfigFacade()).thenReturn(mDeviceConfigFacade);
+        when(mDeviceConfigFacade.getBugReportMinIntervalMs())
+                .thenReturn(BUG_REPORT_MIN_INTERVAL_MS);
+        when(mBuildProperties.isUserBuild()).thenReturn(false);
+        when(mContext.getSystemService(BugreportManager.class)).thenReturn(mBugreportManager);
+        mUwbDiagnostics = new UwbDiagnostics(mContext, mUwbInjector, mBuildProperties);
+    }
+
+    @Test
+    public void takeBugReportDoesNothingOnUserBuild() throws Exception {
+        when(mBuildProperties.isUserBuild()).thenReturn(true);
+        mUwbDiagnostics.takeBugReport("");
+        verify(mBugreportManager, never()).requestBugreport(any(), any(), any());
+    }
+
+    @Test
+    public void takeBugReportTwiceWithInsufficientTimeGapSkipSecondRequest() throws Exception {
+        // 1st attempt should succeed
+        when(mUwbInjector.getElapsedSinceBootMillis()).thenReturn(10L);
+        mUwbDiagnostics.takeBugReport("");
+        verify(mBugreportManager, times(1)).requestBugreport(any(), any(), any());
+        // 2nd attempt should fail
+        when(mUwbInjector.getWallClockMillis()).thenReturn(BUG_REPORT_MIN_INTERVAL_MS - 20L);
+        mUwbDiagnostics.takeBugReport("");
+        verify(mBugreportManager, times(1)).requestBugreport(any(), any(), any());
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/UwbMetricsTest.java b/service/tests/src/com/android/server/uwb/UwbMetricsTest.java
new file mode 100644
index 0000000..c0fe2e4
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/UwbMetricsTest.java
@@ -0,0 +1,296 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import static com.android.server.uwb.DeviceConfigFacade.DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.uwb.RangingMeasurement;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.server.uwb.UwbSessionManager.UwbSession;
+import com.android.server.uwb.data.UwbRangingData;
+import com.android.server.uwb.data.UwbTwoWayMeasurement;
+import com.android.server.uwb.data.UwbUciConstants;
+import com.android.server.uwb.proto.UwbStatsLog;
+
+import com.google.uwb.support.fira.FiraOpenSessionParams;
+import com.google.uwb.support.fira.FiraParams;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+
+/**
+ * Unit tests for {@link com.android.server.uwb.UwbMetrics}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class UwbMetricsTest {
+    private static final int CHANNEL_DEFAULT = 5;
+    private static final int DISTANCE_DEFAULT_CM = 100;
+    private static final int ELEVATION_DEFAULT_DEGREE = 50;
+    private static final int AZIMUTH_DEFAULT_DEGREE = 56;
+    private static final int ELEVATION_FOM_DEFAULT = 90;
+    private static final int AZIMUTH_FOM_DEFAULT = 60;
+    private static final int NLOS_DEFAULT = 1;
+    private static final int VALID_RANGING_COUNT = 5;
+    @Mock
+    private UwbInjector mUwbInjector;
+    @Mock
+    private DeviceConfigFacade mDeviceConfigFacade;
+    private UwbTwoWayMeasurement[] mTwoWayMeasurements = new UwbTwoWayMeasurement[1];
+    @Mock
+    private UwbTwoWayMeasurement mTwoWayMeasurement;
+    @Mock
+    private UwbRangingData mRangingData;
+    @Mock
+    private UwbSession mUwbSession;
+    @Mock
+    private FiraOpenSessionParams mFiraParams;
+
+    private UwbMetrics mUwbMetrics;
+    private MockitoSession mMockSession;
+    private long mElapsedTimeMs;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        setElapsedTimeMs(1000L);
+        mTwoWayMeasurements[0] = mTwoWayMeasurement;
+        when(mRangingData.getSessionId()).thenReturn(1L);
+        when(mRangingData.getNoOfRangingMeasures()).thenReturn(1);
+        when(mRangingData.getRangingMeasuresType()).thenReturn(
+                (int) UwbUciConstants.RANGING_MEASUREMENT_TYPE_TWO_WAY);
+        when(mTwoWayMeasurement.getRangingStatus()).thenReturn(FiraParams.STATUS_CODE_OK);
+        when(mRangingData.getRangingTwoWayMeasures()).thenReturn(mTwoWayMeasurements);
+
+        when(mUwbSession.getSessionId()).thenReturn(1);
+        when(mUwbSession.getProtocolName()).thenReturn(FiraParams.PROTOCOL_NAME);
+        when(mUwbSession.getProfileType()).thenReturn(
+                UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__FIRA);
+        when(mUwbSession.getParams()).thenReturn(mFiraParams);
+        when(mFiraParams.getStsConfig()).thenReturn(FiraParams.STS_CONFIG_STATIC);
+        when(mFiraParams.getDeviceRole()).thenReturn(FiraParams.RANGING_DEVICE_ROLE_INITIATOR);
+        when(mFiraParams.getDeviceType()).thenReturn(FiraParams.RANGING_DEVICE_TYPE_CONTROLLER);
+        when(mFiraParams.getChannelNumber()).thenReturn(CHANNEL_DEFAULT);
+
+        when(mTwoWayMeasurement.getDistance()).thenReturn(DISTANCE_DEFAULT_CM);
+        when(mTwoWayMeasurement.getAoaAzimuth()).thenReturn((float) AZIMUTH_DEFAULT_DEGREE);
+        when(mTwoWayMeasurement.getAoaAzimuthFom()).thenReturn(AZIMUTH_FOM_DEFAULT);
+        when(mTwoWayMeasurement.getAoaElevation()).thenReturn((float) ELEVATION_DEFAULT_DEGREE);
+        when(mTwoWayMeasurement.getAoaElevationFom()).thenReturn(ELEVATION_FOM_DEFAULT);
+        when(mTwoWayMeasurement.getNLoS()).thenReturn(NLOS_DEFAULT);
+        when(mDeviceConfigFacade.getRangingResultLogIntervalMs())
+                .thenReturn(DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS);
+        when(mUwbInjector.getDeviceConfigFacade()).thenReturn(mDeviceConfigFacade);
+
+        mUwbMetrics = new UwbMetrics(mUwbInjector);
+        mMockSession = ExtendedMockito.mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .mockStatic(UwbStatsLog.class)
+                .startMocking();
+    }
+
+    /**
+     * Called after each test
+     */
+    @After
+    public void cleanup() {
+        validateMockitoUsage();
+        mMockSession.finishMocking();
+    }
+
+    private void setElapsedTimeMs(long elapsedTimeMs) {
+        mElapsedTimeMs = elapsedTimeMs;
+        when(mUwbInjector.getElapsedSinceBootMillis()).thenReturn(mElapsedTimeMs);
+    }
+
+    private void addElapsedTimeMs(long durationMs) {
+        mElapsedTimeMs += durationMs;
+        when(mUwbInjector.getElapsedSinceBootMillis()).thenReturn(mElapsedTimeMs);
+    }
+
+    @Test
+    public void testLogRangingSessionAllEvents() throws Exception {
+        mUwbMetrics.logRangingInitEvent(mUwbSession, UwbUciConstants.STATUS_CODE_OK);
+        ExtendedMockito.verify(() -> UwbStatsLog.write(
+                UwbStatsLog.UWB_SESSION_INITED,
+                UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__FIRA,
+                UwbStatsLog.UWB_SESSION_INITIATED__STS__STATIC, true,
+                true, false, true,
+                CHANNEL_DEFAULT, UwbStatsLog.UWB_SESSION_INITIATED__STATUS__SUCCESS,
+                0, 0
+        ));
+
+        mUwbMetrics.longRangingStartEvent(mUwbSession, UwbUciConstants.STATUS_CODE_FAILED);
+        addElapsedTimeMs(DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS);
+        mUwbMetrics.longRangingStartEvent(mUwbSession, UwbUciConstants.STATUS_CODE_OK);
+        addElapsedTimeMs(DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS);
+
+        for (int i = 0; i < VALID_RANGING_COUNT; i++) {
+            addElapsedTimeMs(DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS);
+            mUwbMetrics.logRangingResult(UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__FIRA,
+                    mRangingData);
+        }
+        when(mTwoWayMeasurement.getRangingStatus()).thenReturn(UwbUciConstants.STATUS_CODE_FAILED);
+        mUwbMetrics.logRangingResult(UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__FIRA,
+                mRangingData);
+
+        mUwbMetrics.logRangingCloseEvent(mUwbSession, UwbUciConstants.STATUS_CODE_FAILED);
+        addElapsedTimeMs(DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS);
+        mUwbMetrics.logRangingCloseEvent(mUwbSession, UwbUciConstants.STATUS_CODE_OK);
+
+        ExtendedMockito.verify(() -> UwbStatsLog.write(UwbStatsLog.UWB_FIRST_RANGING_RECEIVED,
+                UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__FIRA,
+                DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS * 2,
+                DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS * 2 / 200));
+
+        ExtendedMockito.verify(() -> UwbStatsLog.write(UwbStatsLog.UWB_SESSION_CLOSED,
+                UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__FIRA,
+                UwbStatsLog.UWB_SESSION_INITIATED__STS__STATIC, true,
+                true, false, true,
+                DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS * (VALID_RANGING_COUNT + 2),
+                UwbStatsLog.UWB_SESSION_CLOSED__DURATION_BUCKET__TEN_SEC_TO_ONE_MIN,
+                VALID_RANGING_COUNT + 1, VALID_RANGING_COUNT,
+                UwbStatsLog.UWB_SESSION_CLOSED__RANGING_COUNT_BUCKET__FIVE_TO_TWENTY,
+                UwbStatsLog.UWB_SESSION_CLOSED__RANGING_COUNT_BUCKET__ONE_TO_FIVE,
+                2, 1, 0));
+    }
+
+    @Test
+    public void testLogRangingSessionInitFiraInvalidParams() throws Exception {
+        when(mFiraParams.getStsConfig()).thenReturn(FiraParams.STS_CONFIG_DYNAMIC);
+        when(mFiraParams.getDeviceRole()).thenReturn(FiraParams.RANGING_DEVICE_ROLE_RESPONDER);
+        when(mFiraParams.getDeviceType()).thenReturn(FiraParams.RANGING_DEVICE_TYPE_CONTROLEE);
+
+        mUwbMetrics.logRangingInitEvent(mUwbSession,
+                UwbUciConstants.STATUS_CODE_INVALID_PARAM);
+        ExtendedMockito.verify(() -> UwbStatsLog.write(
+                UwbStatsLog.UWB_SESSION_INITED,
+                UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__FIRA,
+                UwbStatsLog.UWB_SESSION_INITIATED__STS__DYNAMIC, false,
+                false, false, true,
+                CHANNEL_DEFAULT, UwbStatsLog.UWB_SESSION_INITIATED__STATUS__BAD_PARAMS,
+                0, 0
+        ));
+    }
+
+    @Test
+    public void testLoggingRangingResultValidDistanceAngle() throws Exception {
+        addElapsedTimeMs(DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS);
+        mUwbMetrics.logRangingResult(UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__FIRA,
+                mRangingData);
+
+        ExtendedMockito.verify(() -> UwbStatsLog.write(
+                UwbStatsLog.UWB_RANGING_MEASUREMENT_RECEIVED,
+                UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__FIRA,
+                UwbStatsLog.UWB_RANGING_MEASUREMENT_RECEIVED__NLOS__NLOS,
+                true, DISTANCE_DEFAULT_CM, DISTANCE_DEFAULT_CM / 50,
+                RangingMeasurement.RSSI_UNKNOWN,
+                true, AZIMUTH_DEFAULT_DEGREE, AZIMUTH_DEFAULT_DEGREE / 10, AZIMUTH_FOM_DEFAULT,
+                true, ELEVATION_DEFAULT_DEGREE, ELEVATION_DEFAULT_DEGREE / 10, ELEVATION_FOM_DEFAULT
+        ));
+    }
+
+    @Test
+    public void testLoggingRangingResultSmallLoggingInterval() throws Exception {
+        mUwbMetrics.logRangingResult(UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__FIRA,
+                mRangingData);
+
+        ExtendedMockito.verify(() -> UwbStatsLog.write(
+                UwbStatsLog.UWB_RANGING_MEASUREMENT_RECEIVED,
+                UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__FIRA,
+                UwbStatsLog.UWB_RANGING_MEASUREMENT_RECEIVED__NLOS__NLOS,
+                true, DISTANCE_DEFAULT_CM, DISTANCE_DEFAULT_CM / 50,
+                RangingMeasurement.RSSI_UNKNOWN,
+                true, AZIMUTH_DEFAULT_DEGREE, AZIMUTH_DEFAULT_DEGREE / 10, AZIMUTH_FOM_DEFAULT,
+                true, ELEVATION_DEFAULT_DEGREE, ELEVATION_DEFAULT_DEGREE / 10, ELEVATION_FOM_DEFAULT
+        ), times(0));
+    }
+
+    @Test
+    public void testLoggingRangingResultInvalidDistance() throws Exception {
+        addElapsedTimeMs(DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS);
+        when(mTwoWayMeasurement.getDistance()).thenReturn(UwbMetrics.INVALID_DISTANCE);
+        when(mTwoWayMeasurement.getAoaAzimuth()).thenReturn((float) -10.0);
+        when(mTwoWayMeasurement.getAoaAzimuthFom()).thenReturn(0);
+        when(mTwoWayMeasurement.getAoaElevation()).thenReturn((float) -20.0);
+        when(mTwoWayMeasurement.getAoaElevationFom()).thenReturn(0);
+        when(mTwoWayMeasurement.getNLoS()).thenReturn(0);
+
+        mUwbMetrics.logRangingResult(UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__CCC,
+                mRangingData);
+
+        ExtendedMockito.verify(() -> UwbStatsLog.write(
+                UwbStatsLog.UWB_RANGING_MEASUREMENT_RECEIVED,
+                UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__CCC,
+                UwbStatsLog.UWB_RANGING_MEASUREMENT_RECEIVED__NLOS__LOS,
+                false, UwbMetrics.INVALID_DISTANCE, 0,
+                RangingMeasurement.RSSI_UNKNOWN,
+                false, -10, 0, 0,
+                false, -20, 0, 0
+        ));
+    }
+
+    @Test
+    public void testReportDeviceSuccessErrorCount() throws Exception {
+        mUwbMetrics.incrementDeviceInitFailureCount();
+        ExtendedMockito.verify(() -> UwbStatsLog.write(UwbStatsLog.UWB_DEVICE_ERROR_REPORTED,
+                UwbStatsLog.UWB_DEVICE_ERROR_REPORTED__TYPE__INIT_ERROR));
+        mUwbMetrics.incrementDeviceInitSuccessCount();
+        mUwbMetrics.incrementDeviceStatusErrorCount();
+        ExtendedMockito.verify(() -> UwbStatsLog.write(UwbStatsLog.UWB_DEVICE_ERROR_REPORTED,
+                UwbStatsLog.UWB_DEVICE_ERROR_REPORTED__TYPE__DEVICE_STATUS_ERROR));
+        mUwbMetrics.incrementUciGenericErrorCount();
+        ExtendedMockito.verify(() -> UwbStatsLog.write(UwbStatsLog.UWB_DEVICE_ERROR_REPORTED,
+                UwbStatsLog.UWB_DEVICE_ERROR_REPORTED__TYPE__UCI_GENERIC_ERROR));
+    }
+
+    @Test
+    public void testDumpStatsNoCrash() throws Exception {
+        mUwbMetrics.logRangingInitEvent(mUwbSession, UwbUciConstants.STATUS_CODE_OK);
+        mUwbMetrics.logRangingInitEvent(mUwbSession,
+                UwbUciConstants.STATUS_CODE_INVALID_PARAM);
+
+        addElapsedTimeMs(DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS);
+        mUwbMetrics.logRangingResult(UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__CCC, mRangingData);
+        addElapsedTimeMs(DEFAULT_RANGING_RESULT_LOG_INTERVAL_MS);
+        mUwbMetrics.logRangingResult(UwbStatsLog.UWB_SESSION_INITIATED__PROFILE__FIRA,
+                mRangingData);
+
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        PrintWriter writer = new PrintWriter(stream);
+        mUwbMetrics.dump(null, writer, null);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java b/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java
new file mode 100644
index 0000000..e25f7ab
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java
@@ -0,0 +1,676 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.uwb.support.ccc.CccParams.CHAPS_PER_SLOT_3;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_CONFIG_MODE_NONE;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_SEQUENCE_DEFAULT;
+import static com.google.uwb.support.ccc.CccParams.PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE;
+import static com.google.uwb.support.ccc.CccParams.SLOTS_PER_ROUND_6;
+import static com.google.uwb.support.ccc.CccParams.UWB_CHANNEL_9;
+import static com.google.uwb.support.fira.FiraParams.MULTICAST_LIST_UPDATE_ACTION_ADD;
+import static com.google.uwb.support.fira.FiraParams.MULTICAST_LIST_UPDATE_ACTION_DELETE;
+import static com.google.uwb.support.fira.FiraParams.MULTI_NODE_MODE_UNICAST;
+import static com.google.uwb.support.fira.FiraParams.RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY;
+import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_ROLE_RESPONDER;
+import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_TYPE_CONTROLLER;
+import static com.google.uwb.support.fira.FiraParams.RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE;
+
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Pair;
+import android.uwb.AdapterState;
+import android.uwb.IUwbAdapterStateCallbacks;
+import android.uwb.IUwbRangingCallbacks;
+import android.uwb.IUwbVendorUciCallback;
+import android.uwb.SessionHandle;
+import android.uwb.StateChangeReason;
+import android.uwb.UwbAddress;
+import android.uwb.UwbManager;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.uwb.data.UwbUciConstants;
+import com.android.server.uwb.data.UwbVendorUciResponse;
+import com.android.server.uwb.jni.NativeUwbManager;
+
+import com.google.uwb.support.ccc.CccOpenRangingParams;
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.ccc.CccPulseShapeCombo;
+import com.google.uwb.support.ccc.CccStartRangingParams;
+import com.google.uwb.support.fira.FiraControleeParams;
+import com.google.uwb.support.fira.FiraOpenSessionParams;
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraRangingReconfigureParams;
+import com.google.uwb.support.generic.GenericParams;
+import com.google.uwb.support.generic.GenericSpecificationParams;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+
+/**
+ * Tests for {@link UwbServiceCore}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class UwbServiceCoreTest {
+    private static final int TEST_UID = 44;
+    private static final String TEST_PACKAGE_NAME = "com.android.uwb";
+    private static final AttributionSource TEST_ATTRIBUTION_SOURCE =
+            new AttributionSource.Builder(TEST_UID)
+                    .setPackageName(TEST_PACKAGE_NAME)
+                    .build();
+    private static final FiraOpenSessionParams.Builder TEST_FIRA_OPEN_SESSION_PARAMS =
+            new FiraOpenSessionParams.Builder()
+                    .setProtocolVersion(FiraParams.PROTOCOL_VERSION_1_1)
+                    .setSessionId(1)
+                    .setDeviceType(RANGING_DEVICE_TYPE_CONTROLLER)
+                    .setDeviceRole(RANGING_DEVICE_ROLE_RESPONDER)
+                    .setDeviceAddress(UwbAddress.fromBytes(new byte[] { 0x4, 0x6}))
+                    .setDestAddressList(Arrays.asList(UwbAddress.fromBytes(new byte[] { 0x4, 0x6})))
+                    .setMultiNodeMode(MULTI_NODE_MODE_UNICAST)
+                    .setRangingRoundUsage(RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE)
+                    .setVendorId(new byte[]{0x5, 0x78})
+                    .setStaticStsIV(new byte[]{0x1a, 0x55, 0x77, 0x47, 0x7e, 0x7d});
+
+    @VisibleForTesting
+    private static final CccOpenRangingParams.Builder TEST_CCC_OPEN_RANGING_PARAMS =
+            new CccOpenRangingParams.Builder()
+                    .setProtocolVersion(CccParams.PROTOCOL_VERSION_1_0)
+                    .setUwbConfig(CccParams.UWB_CONFIG_0)
+                    .setPulseShapeCombo(
+                            new CccPulseShapeCombo(
+                                    PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE,
+                                    PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE))
+                    .setSessionId(1)
+                    .setRanMultiplier(4)
+                    .setChannel(UWB_CHANNEL_9)
+                    .setNumChapsPerSlot(CHAPS_PER_SLOT_3)
+                    .setNumResponderNodes(1)
+                    .setNumSlotsPerRound(SLOTS_PER_ROUND_6)
+                    .setSyncCodeIndex(1)
+                    .setHoppingConfigMode(HOPPING_CONFIG_MODE_NONE)
+                    .setHoppingSequence(HOPPING_SEQUENCE_DEFAULT);
+    @Mock private Context mContext;
+    @Mock private NativeUwbManager mNativeUwbManager;
+    @Mock private UwbMetrics mUwbMetrics;
+    @Mock private UwbCountryCode mUwbCountryCode;
+    @Mock private UwbSessionManager mUwbSessionManager;
+    @Mock private UwbConfigurationManager mUwbConfigurationManager;
+    @Mock private UwbInjector mUwbInjector;
+    @Mock DeviceConfigFacade mDeviceConfigFacade;
+    private TestLooper mTestLooper;
+    private MockitoSession mMockitoSession;
+
+    private UwbServiceCore mUwbServiceCore;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mTestLooper = new TestLooper();
+        PowerManager powerManager = mock(PowerManager.class);
+        when(powerManager.newWakeLock(anyInt(), anyString()))
+                .thenReturn(mock(PowerManager.WakeLock.class));
+        when(mContext.getSystemService(PowerManager.class)).thenReturn(powerManager);
+        when(mUwbInjector.isSystemApp(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(true);
+        when(mUwbInjector.isForegroundAppOrService(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(true);
+        when(mUwbInjector.getDeviceConfigFacade()).thenReturn(mDeviceConfigFacade);
+        when(mDeviceConfigFacade.getBugReportMinIntervalMs())
+                .thenReturn(DeviceConfigFacade.DEFAULT_BUG_REPORT_MIN_INTERVAL_MS);
+        mUwbServiceCore = new UwbServiceCore(mContext, mNativeUwbManager, mUwbMetrics,
+                mUwbCountryCode, mUwbSessionManager, mUwbConfigurationManager,
+                mUwbInjector, mTestLooper.getLooper());
+
+        // static mocking for executor service.
+        mMockitoSession = ExtendedMockito.mockitoSession()
+                .mockStatic(Executors.class, Mockito.withSettings().lenient())
+                .strictness(Strictness.LENIENT)
+                .startMocking();
+        ExecutorService executorService = mock(ExecutorService.class);
+        doAnswer(invocation -> {
+            FutureTask t = invocation.getArgument(1);
+            t.run();
+            return t;
+        }).when(executorService).submit(any(Callable.class));
+        doAnswer(invocation -> {
+            FutureTask t = invocation.getArgument(0);
+            t.run();
+            return t;
+        }).when(executorService).submit(any(Runnable.class));
+        when(Executors.newSingleThreadExecutor()).thenReturn(executorService);
+    }
+
+    /**
+     * Called after each test
+     */
+    @After
+    public void cleanup() {
+        validateMockitoUsage();
+        if (mMockitoSession != null) {
+            mMockitoSession.finishMocking();
+        }
+    }
+
+    private void verifyGetSpecificationInfoSuccess() throws Exception {
+        GenericSpecificationParams genericSpecificationParams =
+                mock(GenericSpecificationParams.class);
+        PersistableBundle genericSpecificationBundle = mock(PersistableBundle.class);
+        when(genericSpecificationParams.toBundle()).thenReturn(genericSpecificationBundle);
+
+        when(mUwbConfigurationManager.getCapsInfo(eq(GenericParams.PROTOCOL_NAME), any()))
+                .thenReturn(Pair.create(
+                        UwbUciConstants.STATUS_CODE_OK, genericSpecificationParams));
+
+        PersistableBundle specifications = mUwbServiceCore.getSpecificationInfo();
+        assertThat(specifications).isEqualTo(genericSpecificationBundle);
+        verify(mUwbConfigurationManager).getCapsInfo(eq(GenericParams.PROTOCOL_NAME), any());
+    }
+
+    @Test
+    public void testGetSpecificationInfoSuccess() throws Exception {
+        verifyGetSpecificationInfoSuccess();
+    }
+
+    private void enableUwb() throws Exception {
+        when(mNativeUwbManager.doInitialize()).thenReturn(true);
+        when(mUwbCountryCode.setCountryCode(anyBoolean())).thenReturn(true);
+
+        mUwbServiceCore.setEnabled(true);
+        mTestLooper.dispatchAll();
+    }
+
+    private void disableUwb() throws Exception {
+        when(mNativeUwbManager.doDeinitialize()).thenReturn(true);
+
+        mUwbServiceCore.setEnabled(false);
+        mTestLooper.dispatchAll();
+    }
+
+    @Test
+    public void testEnable() throws Exception {
+        IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class);
+        when(cb.asBinder()).thenReturn(mock(IBinder.class));
+        mUwbServiceCore.registerAdapterStateCallbacks(cb);
+
+        enableUwb();
+
+        verify(mNativeUwbManager).doInitialize();
+        verify(mUwbCountryCode).setCountryCode(true);
+        verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE,
+                StateChangeReason.SYSTEM_POLICY);
+    }
+
+    @Test
+    public void testEnableWhenAlreadyEnabled() throws Exception {
+        IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class);
+        when(cb.asBinder()).thenReturn(mock(IBinder.class));
+        mUwbServiceCore.registerAdapterStateCallbacks(cb);
+
+        enableUwb();
+
+        verify(mNativeUwbManager).doInitialize();
+        verify(mUwbCountryCode).setCountryCode(true);
+        verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE,
+                StateChangeReason.SYSTEM_POLICY);
+
+        clearInvocations(mNativeUwbManager, mUwbCountryCode, cb);
+        // Enable again. should be ignored.
+        enableUwb();
+        verifyNoMoreInteractions(mNativeUwbManager, mUwbCountryCode, cb);
+    }
+
+
+    @Test
+    public void testDisable() throws Exception {
+        IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class);
+        when(cb.asBinder()).thenReturn(mock(IBinder.class));
+        mUwbServiceCore.registerAdapterStateCallbacks(cb);
+
+        // Enable first
+        enableUwb();
+
+        disableUwb();
+
+        verify(mNativeUwbManager).doDeinitialize();
+        verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_DISABLED,
+                StateChangeReason.SYSTEM_POLICY);
+    }
+
+
+    @Test
+    public void testDisableWhenAlreadyDisabled() throws Exception {
+        when(mNativeUwbManager.doInitialize()).thenReturn(true);
+        when(mUwbCountryCode.setCountryCode(anyBoolean())).thenReturn(true);
+        when(mNativeUwbManager.doDeinitialize()).thenReturn(true);
+
+        IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class);
+        when(cb.asBinder()).thenReturn(mock(IBinder.class));
+        mUwbServiceCore.registerAdapterStateCallbacks(cb);
+
+        // Enable first
+        enableUwb();
+
+        disableUwb();
+
+        verify(mNativeUwbManager).doDeinitialize();
+        verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_DISABLED,
+                StateChangeReason.SYSTEM_POLICY);
+
+        clearInvocations(mNativeUwbManager, mUwbCountryCode, cb);
+        // Disable again. should be ignored.
+        disableUwb();
+        verifyNoMoreInteractions(mNativeUwbManager, mUwbCountryCode, cb);
+    }
+
+    @Test
+    public void testOpenFiraRanging() throws Exception {
+        enableUwb();
+
+        SessionHandle sessionHandle = mock(SessionHandle.class);
+        IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class);
+        AttributionSource attributionSource = TEST_ATTRIBUTION_SOURCE;
+        FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build();
+        mUwbServiceCore.openRanging(
+                attributionSource, sessionHandle, cb, params.toBundle());
+
+        verify(mUwbSessionManager).initSession(
+                eq(attributionSource),
+                eq(sessionHandle), eq(params.getSessionId()), eq(FiraParams.PROTOCOL_NAME),
+                argThat(p -> ((FiraOpenSessionParams) p).getSessionId() == params.getSessionId()),
+                eq(cb));
+
+    }
+
+    @Test
+    public void testOpenCccRanging() throws Exception {
+        enableUwb();
+
+        SessionHandle sessionHandle = mock(SessionHandle.class);
+        IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class);
+        CccOpenRangingParams params = TEST_CCC_OPEN_RANGING_PARAMS.build();
+        AttributionSource attributionSource = TEST_ATTRIBUTION_SOURCE;
+        mUwbServiceCore.openRanging(
+                attributionSource, sessionHandle, cb, params.toBundle());
+
+        verify(mUwbSessionManager).initSession(
+                eq(attributionSource),
+                eq(sessionHandle), eq(params.getSessionId()), eq(CccParams.PROTOCOL_NAME),
+                argThat(p -> ((CccOpenRangingParams) p).getSessionId() == params.getSessionId()),
+                eq(cb));
+    }
+
+    @Test
+    public void testOpenRangingWhenUwbDisabled() throws Exception {
+        SessionHandle sessionHandle = mock(SessionHandle.class);
+        IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class);
+        CccOpenRangingParams params = TEST_CCC_OPEN_RANGING_PARAMS.build();
+        AttributionSource attributionSource = TEST_ATTRIBUTION_SOURCE;
+
+        try {
+            mUwbServiceCore.openRanging(attributionSource, sessionHandle, cb, params.toBundle());
+            fail();
+        } catch (IllegalStateException e) {
+            // pass
+        }
+
+        // Should be ignored.
+        verifyNoMoreInteractions(mUwbSessionManager);
+    }
+
+    @Test
+    public void testOpenRangingWithNonSystemAppInFg() throws Exception {
+        enableUwb();
+
+        when(mUwbInjector.isSystemApp(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(false);
+        when(mUwbInjector.isForegroundAppOrService(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(true);
+
+        SessionHandle sessionHandle = mock(SessionHandle.class);
+        IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class);
+        AttributionSource attributionSource = TEST_ATTRIBUTION_SOURCE;
+        FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build();
+        mUwbServiceCore.openRanging(
+                attributionSource, sessionHandle, cb, params.toBundle());
+
+        verify(mUwbSessionManager).initSession(
+                eq(attributionSource),
+                eq(sessionHandle), eq(params.getSessionId()), eq(FiraParams.PROTOCOL_NAME),
+                argThat(p -> ((FiraOpenSessionParams) p).getSessionId() == params.getSessionId()),
+                eq(cb));
+    }
+
+    @Test
+    public void testOpenRangingWithNonSystemAppNotInFg() throws Exception {
+        enableUwb();
+
+        when(mUwbInjector.isSystemApp(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(false);
+        when(mUwbInjector.isForegroundAppOrService(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(false);
+
+        SessionHandle sessionHandle = mock(SessionHandle.class);
+        IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class);
+        AttributionSource attributionSource = TEST_ATTRIBUTION_SOURCE;
+        FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build();
+        mUwbServiceCore.openRanging(
+                attributionSource, sessionHandle, cb, params.toBundle());
+
+        verify(mUwbSessionManager, never()).initSession(
+                any(), any(), anyInt(), any(), any(), any());
+        verify(cb).onRangingOpenFailed(
+                eq(sessionHandle), eq(StateChangeReason.SYSTEM_POLICY), any());
+    }
+
+    @Test
+    public void testOpenRangingWithNonSystemAppInFgInChain() throws Exception {
+        enableUwb();
+
+        int test_uid_2 = 67;
+        String test_package_name_2 = "com.android.uwb.2";
+        when(mUwbInjector.isSystemApp(test_uid_2, test_package_name_2)).thenReturn(false);
+        when(mUwbInjector.isForegroundAppOrService(test_uid_2, test_package_name_2))
+                .thenReturn(true);
+
+        SessionHandle sessionHandle = mock(SessionHandle.class);
+        IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class);
+        // simulate system app triggered the request on behalf of a fg app in fg.
+        AttributionSource attributionSource = new AttributionSource.Builder(TEST_UID)
+                .setPackageName(TEST_PACKAGE_NAME)
+                .setNext(new AttributionSource.Builder(test_uid_2)
+                        .setPackageName(test_package_name_2)
+                        .build())
+                .build();
+        FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build();
+        mUwbServiceCore.openRanging(
+                attributionSource, sessionHandle, cb, params.toBundle());
+
+        verify(mUwbSessionManager).initSession(
+                eq(attributionSource),
+                eq(sessionHandle), eq(params.getSessionId()), eq(FiraParams.PROTOCOL_NAME),
+                argThat(p -> ((FiraOpenSessionParams) p).getSessionId() == params.getSessionId()),
+                eq(cb));
+    }
+
+    @Test
+    public void testOpenRangingWithNonSystemAppNotInFgInChain() throws Exception {
+        enableUwb();
+
+        int test_uid_2 = 67;
+        String test_package_name_2 = "com.android.uwb.2";
+        when(mUwbInjector.isSystemApp(test_uid_2, test_package_name_2)).thenReturn(false);
+        when(mUwbInjector.isForegroundAppOrService(test_uid_2, test_package_name_2))
+                .thenReturn(false);
+
+        SessionHandle sessionHandle = mock(SessionHandle.class);
+        IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class);
+        // simulate system app triggered the request on behalf of a fg app not in fg.
+        AttributionSource attributionSource = new AttributionSource.Builder(TEST_UID)
+                .setPackageName(TEST_PACKAGE_NAME)
+                .setNext(new AttributionSource.Builder(test_uid_2)
+                        .setPackageName(test_package_name_2)
+                        .build())
+                .build();
+        FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build();
+        mUwbServiceCore.openRanging(
+                attributionSource, sessionHandle, cb, params.toBundle());
+
+        verify(mUwbSessionManager, never()).initSession(
+                any(), any(), anyInt(), any(), any(), any());
+        verify(cb).onRangingOpenFailed(
+                eq(sessionHandle), eq(StateChangeReason.SYSTEM_POLICY), any());
+    }
+
+    @Test
+    public void testStartCccRanging() throws Exception {
+        enableUwb();
+
+        SessionHandle sessionHandle = mock(SessionHandle.class);
+        CccStartRangingParams params = new CccStartRangingParams.Builder()
+                .setRanMultiplier(6)
+                .setSessionId(1)
+                .build();
+        mUwbServiceCore.startRanging(sessionHandle, params.toBundle());
+
+        verify(mUwbSessionManager).startRanging(eq(sessionHandle),
+                argThat(p -> ((CccStartRangingParams) p).getSessionId() == params.getSessionId()));
+    }
+
+    @Test
+    public void testStartCccRangingWithNoStartParams() throws Exception {
+        enableUwb();
+
+        SessionHandle sessionHandle = mock(SessionHandle.class);
+        mUwbServiceCore.startRanging(sessionHandle, new PersistableBundle());
+
+        verify(mUwbSessionManager).startRanging(eq(sessionHandle), argThat(p -> (p == null)));
+    }
+
+    @Test
+    public void testReconfigureRanging() throws Exception {
+        enableUwb();
+
+        SessionHandle sessionHandle = mock(SessionHandle.class);
+        final FiraRangingReconfigureParams parameters =
+                new FiraRangingReconfigureParams.Builder()
+                        .setBlockStrideLength(6)
+                        .setRangeDataNtfConfig(RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY)
+                        .setRangeDataProximityFar(6)
+                        .setRangeDataProximityNear(4)
+                        .build();
+        mUwbServiceCore.reconfigureRanging(sessionHandle, parameters.toBundle());
+        verify(mUwbSessionManager).reconfigure(eq(sessionHandle),
+                argThat((x) ->
+                        ((FiraRangingReconfigureParams) x).getBlockStrideLength().equals(6)));
+    }
+
+    @Test
+    public void testAddControlee() throws Exception {
+        enableUwb();
+
+        SessionHandle sessionHandle = mock(SessionHandle.class);
+        UwbAddress uwbAddress1 = UwbAddress.fromBytes(new byte[] {1, 2});
+        UwbAddress uwbAddress2 = UwbAddress.fromBytes(new byte[] {4, 5});
+        UwbAddress[] addressList = new UwbAddress[] {uwbAddress1, uwbAddress2};
+        int[] subSessionIdList = new int[] {3, 4};
+        FiraControleeParams params =
+                new FiraControleeParams.Builder()
+                        .setAddressList(addressList)
+                        .setSubSessionIdList(subSessionIdList)
+                        .build();
+
+        mUwbServiceCore.addControlee(sessionHandle, params.toBundle());
+        verify(mUwbSessionManager).reconfigure(eq(sessionHandle),
+                argThat((x) -> {
+                    FiraRangingReconfigureParams reconfigureParams =
+                            (FiraRangingReconfigureParams) x;
+                    return reconfigureParams.getAction().equals(MULTICAST_LIST_UPDATE_ACTION_ADD)
+                            && Arrays.equals(
+                                    reconfigureParams.getAddressList(), params.getAddressList())
+                            && Arrays.equals(
+                                    reconfigureParams.getSubSessionIdList(),
+                                    params.getSubSessionIdList());
+                }));
+    }
+
+    @Test
+    public void testRemoveControlee() throws Exception {
+        enableUwb();
+
+        SessionHandle sessionHandle = mock(SessionHandle.class);
+        UwbAddress uwbAddress1 = UwbAddress.fromBytes(new byte[] {1, 2});
+        UwbAddress uwbAddress2 = UwbAddress.fromBytes(new byte[] {4, 5});
+        UwbAddress[] addressList = new UwbAddress[] {uwbAddress1, uwbAddress2};
+        int[] subSessionIdList = new int[] {3, 4};
+        FiraControleeParams params =
+                new FiraControleeParams.Builder()
+                        .setAddressList(addressList)
+                        .setSubSessionIdList(subSessionIdList)
+                        .build();
+
+        mUwbServiceCore.removeControlee(sessionHandle, params.toBundle());
+        verify(mUwbSessionManager).reconfigure(eq(sessionHandle),
+                argThat((x) -> {
+                    FiraRangingReconfigureParams reconfigureParams =
+                            (FiraRangingReconfigureParams) x;
+                    return reconfigureParams.getAction().equals(MULTICAST_LIST_UPDATE_ACTION_DELETE)
+                            && Arrays.equals(
+                                    reconfigureParams.getAddressList(), params.getAddressList())
+                            && Arrays.equals(
+                                    reconfigureParams.getSubSessionIdList(),
+                                    params.getSubSessionIdList());
+                }));
+    }
+
+    @Test
+    public void testStopRanging() throws Exception {
+        enableUwb();
+
+        SessionHandle sessionHandle = mock(SessionHandle.class);
+        mUwbServiceCore.stopRanging(sessionHandle);
+
+        verify(mUwbSessionManager).stopRanging(sessionHandle);
+    }
+
+
+    @Test
+    public void testCloseRanging() throws Exception {
+        enableUwb();
+
+        SessionHandle sessionHandle = mock(SessionHandle.class);
+        mUwbServiceCore.closeRanging(sessionHandle);
+
+        verify(mUwbSessionManager).deInitSession(sessionHandle);
+    }
+
+    @Test
+    public void testGetAdapterState() throws Exception {
+        enableUwb();
+        assertThat(mUwbServiceCore.getAdapterState())
+                .isEqualTo(AdapterState.STATE_ENABLED_INACTIVE);
+
+        disableUwb();
+        assertThat(mUwbServiceCore.getAdapterState())
+                .isEqualTo(AdapterState.STATE_DISABLED);
+    }
+
+
+    @Test
+    public void testSendVendorUciCommand() throws Exception {
+        enableUwb();
+
+        int gid = 0;
+        int oid = 0;
+        byte[] payload = new byte[0];
+        UwbVendorUciResponse rsp = new UwbVendorUciResponse(
+                (byte) UwbUciConstants.STATUS_CODE_OK, gid, oid, payload);
+        when(mNativeUwbManager.sendRawVendorCmd(anyInt(), anyInt(), any()))
+                .thenReturn(rsp);
+
+        IUwbVendorUciCallback vendorCb = mock(IUwbVendorUciCallback.class);
+        mUwbServiceCore.registerVendorExtensionCallback(vendorCb);
+
+        assertThat(mUwbServiceCore.sendVendorUciMessage(0, 0, new byte[0]))
+                .isEqualTo(UwbUciConstants.STATUS_CODE_OK);
+
+        verify(vendorCb).onVendorResponseReceived(gid, oid, payload);
+    }
+
+    @Test
+    public void testDeviceStateCallback() throws Exception {
+        IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class);
+        when(cb.asBinder()).thenReturn(mock(IBinder.class));
+        mUwbServiceCore.registerAdapterStateCallbacks(cb);
+
+        enableUwb();
+        verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE,
+                StateChangeReason.SYSTEM_POLICY);
+
+        mUwbServiceCore.onDeviceStatusNotificationReceived(UwbUciConstants.DEVICE_STATE_ACTIVE);
+        verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_ENABLED_ACTIVE,
+                StateChangeReason.SESSION_STARTED);
+    }
+
+    @Test
+    public void testToggleOfOnDeviceStateErrorCallback() throws Exception {
+        IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class);
+        when(cb.asBinder()).thenReturn(mock(IBinder.class));
+        mUwbServiceCore.registerAdapterStateCallbacks(cb);
+
+        enableUwb();
+        verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE,
+                StateChangeReason.SYSTEM_POLICY);
+
+        when(mNativeUwbManager.doDeinitialize()).thenReturn(true);
+
+        mUwbServiceCore.onDeviceStatusNotificationReceived(UwbUciConstants.DEVICE_STATE_ERROR);
+        mTestLooper.dispatchAll();
+        // Verify UWB toggle off.
+        verify(mNativeUwbManager).doDeinitialize();
+        verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_DISABLED,
+                StateChangeReason.SYSTEM_POLICY);
+    }
+
+    @Test
+    public void testVendorUciNotificationCallback() throws Exception {
+        enableUwb();
+
+        IUwbVendorUciCallback vendorCb = mock(IUwbVendorUciCallback.class);
+        mUwbServiceCore.registerVendorExtensionCallback(vendorCb);
+        int gid = 0;
+        int oid = 0;
+        byte[] payload = new byte[0];
+        mUwbServiceCore.onVendorUciNotificationReceived(gid, oid, payload);
+        verify(vendorCb).onVendorNotificationReceived(gid, oid, payload);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/UwbServiceImplTest.java b/service/tests/src/com/android/server/uwb/UwbServiceImplTest.java
new file mode 100644
index 0000000..cdc57c8
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/UwbServiceImplTest.java
@@ -0,0 +1,560 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import static android.Manifest.permission.UWB_PRIVILEGED;
+import static android.uwb.UwbManager.AdapterStateCallback.STATE_ENABLED_ACTIVE;
+import static android.uwb.UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE;
+
+import static com.android.server.uwb.UwbSettingsStore.SETTINGS_TOGGLE_STATE;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.uwb.support.fira.FiraParams.RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.AttributionSource;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.uwb.IUwbAdapterStateCallbacks;
+import android.uwb.IUwbAdfProvisionStateCallbacks;
+import android.uwb.IUwbRangingCallbacks;
+import android.uwb.IUwbVendorUciCallback;
+import android.uwb.SessionHandle;
+import android.uwb.UwbAddress;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.jni.NativeUwbManager;
+import com.android.server.uwb.multchip.UwbMultichipData;
+
+import com.google.uwb.support.fira.FiraRangingReconfigureParams;
+import com.google.uwb.support.multichip.ChipInfoParams;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+/**
+ * Tests for {@link UwbServiceImpl}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class UwbServiceImplTest {
+    private static final int UID = 343453;
+    private static final String PACKAGE_NAME = "com.uwb.test";
+    private static final String DEFAULT_CHIP_ID = "defaultChipId";
+    private static final ChipInfoParams DEFAULT_CHIP_INFO_PARAMS =
+            ChipInfoParams.createBuilder().setChipId(DEFAULT_CHIP_ID).build();
+    private static final AttributionSource ATTRIBUTION_SOURCE =
+            new AttributionSource.Builder(UID).setPackageName(PACKAGE_NAME).build();
+
+    @Mock private UwbServiceCore mUwbServiceCore;
+    @Mock private Context mContext;
+    @Mock private UwbInjector mUwbInjector;
+    @Mock private UwbSettingsStore mUwbSettingsStore;
+    @Mock private NativeUwbManager mNativeUwbManager;
+    @Mock private UwbMultichipData mUwbMultichipData;
+    @Captor private ArgumentCaptor<IUwbRangingCallbacks> mRangingCbCaptor;
+    @Captor private ArgumentCaptor<IUwbRangingCallbacks> mRangingCbCaptor2;
+    @Captor private ArgumentCaptor<IBinder.DeathRecipient> mClientDeathCaptor;
+    @Captor private ArgumentCaptor<IBinder.DeathRecipient> mUwbServiceCoreDeathCaptor;
+    @Captor private ArgumentCaptor<BroadcastReceiver> mApmModeBroadcastReceiver;
+
+    private UwbServiceImpl mUwbServiceImpl;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        when(mUwbInjector.getUwbSettingsStore()).thenReturn(mUwbSettingsStore);
+        when(mUwbSettingsStore.get(SETTINGS_TOGGLE_STATE)).thenReturn(true);
+        when(mUwbMultichipData.getChipInfos()).thenReturn(List.of(DEFAULT_CHIP_INFO_PARAMS));
+        when(mUwbMultichipData.getDefaultChipId()).thenReturn(DEFAULT_CHIP_ID);
+        when(mUwbInjector.getUwbServiceCore()).thenReturn(mUwbServiceCore);
+        when(mUwbInjector.getMultichipData()).thenReturn(mUwbMultichipData);
+        when(mUwbInjector.getSettingsInt(Settings.Global.AIRPLANE_MODE_ON, 0)).thenReturn(0);
+        when(mUwbInjector.getNativeUwbManager()).thenReturn(mNativeUwbManager);
+
+        mUwbServiceImpl = new UwbServiceImpl(mContext, mUwbInjector);
+
+        verify(mContext).registerReceiver(
+                mApmModeBroadcastReceiver.capture(),
+                argThat(i -> i.getAction(0).equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)));
+    }
+
+    @Test
+    public void testRegisterAdapterStateCallbacks() throws Exception {
+        final IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class);
+        mUwbServiceImpl.registerAdapterStateCallbacks(cb);
+
+        verify(mUwbServiceCore).registerAdapterStateCallbacks(cb);
+    }
+
+    @Test
+    public void testUnregisterAdapterStateCallbacks() throws Exception {
+        final IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class);
+        mUwbServiceImpl.unregisterAdapterStateCallbacks(cb);
+
+        verify(mUwbServiceCore).unregisterAdapterStateCallbacks(cb);
+    }
+
+    @Test
+    public void testGetTimestampResolutionNanos() throws Exception {
+        final long timestamp = 34L;
+        when(mUwbServiceCore.getTimestampResolutionNanos()).thenReturn(timestamp);
+        assertThat(mUwbServiceImpl.getTimestampResolutionNanos(/* chipId= */ null))
+                .isEqualTo(timestamp);
+
+        verify(mUwbServiceCore).getTimestampResolutionNanos();
+    }
+
+    @Test
+    public void testGetTimestampResolutionNanos_validChipId() throws Exception {
+        final long timestamp = 34L;
+        when(mUwbServiceCore.getTimestampResolutionNanos()).thenReturn(timestamp);
+        assertThat(mUwbServiceImpl.getTimestampResolutionNanos(DEFAULT_CHIP_ID))
+                .isEqualTo(timestamp);
+
+        verify(mUwbServiceCore).getTimestampResolutionNanos();
+    }
+
+    @Test
+    public void testGetTimestampResolutionNanos_invalidChipId() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mUwbServiceImpl.getTimestampResolutionNanos("invalidChipId"));
+    }
+
+    @Test
+    public void testGetSpecificationInfo() throws Exception {
+        final PersistableBundle specification = new PersistableBundle();
+        when(mUwbServiceCore.getSpecificationInfo()).thenReturn(specification);
+        assertThat(mUwbServiceImpl.getSpecificationInfo(/* chipId= */ null))
+                .isEqualTo(specification);
+
+        verify(mUwbServiceCore).getSpecificationInfo();
+    }
+
+    @Test
+    public void testGetSpecificationInfo_validChipId() throws Exception {
+        final PersistableBundle specification = new PersistableBundle();
+        when(mUwbServiceCore.getSpecificationInfo()).thenReturn(specification);
+        assertThat(mUwbServiceImpl.getSpecificationInfo(DEFAULT_CHIP_ID))
+                .isEqualTo(specification);
+
+        verify(mUwbServiceCore).getSpecificationInfo();
+    }
+
+    @Test
+    public void testGetSpecificationInfo_invalidChipId() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mUwbServiceImpl.getSpecificationInfo("invalidChipId"));
+    }
+
+    @Test
+    public void testOpenRanging() throws Exception {
+        final SessionHandle sessionHandle = new SessionHandle(5);
+        final IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class);
+        final PersistableBundle parameters = new PersistableBundle();
+        final IBinder cbBinder = mock(IBinder.class);
+        when(cb.asBinder()).thenReturn(cbBinder);
+
+        mUwbServiceImpl.openRanging(
+                ATTRIBUTION_SOURCE, sessionHandle, cb, parameters, /* chipId= */ null);
+
+        verify(mUwbServiceCore).openRanging(
+                eq(ATTRIBUTION_SOURCE), eq(sessionHandle), mRangingCbCaptor.capture(),
+                eq(parameters));
+        assertThat(mRangingCbCaptor.getValue()).isNotNull();
+    }
+
+    @Test
+    public void testStartRanging() throws Exception {
+        final SessionHandle sessionHandle = new SessionHandle(5);
+        final PersistableBundle parameters = new PersistableBundle();
+
+        mUwbServiceImpl.startRanging(sessionHandle, parameters);
+
+        verify(mUwbServiceCore).startRanging(sessionHandle, parameters);
+    }
+
+    @Test
+    public void testReconfigureRanging() throws Exception {
+        final SessionHandle sessionHandle = new SessionHandle(5);
+        final FiraRangingReconfigureParams parameters =
+                new FiraRangingReconfigureParams.Builder()
+                        .setBlockStrideLength(6)
+                        .setRangeDataNtfConfig(RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY)
+                        .setRangeDataProximityFar(6)
+                        .setRangeDataProximityNear(4)
+                        .build();
+        mUwbServiceImpl.reconfigureRanging(sessionHandle, parameters.toBundle());
+        verify(mUwbServiceCore).reconfigureRanging(eq(sessionHandle),
+                argThat((x) -> x.getInt("update_block_stride_length") == 6));
+    }
+
+    @Test
+    public void testStopRanging() throws Exception {
+        final SessionHandle sessionHandle = new SessionHandle(5);
+
+        mUwbServiceImpl.stopRanging(sessionHandle);
+
+        verify(mUwbServiceCore).stopRanging(sessionHandle);
+    }
+
+    @Test
+    public void testCloseRanging() throws Exception {
+        final SessionHandle sessionHandle = new SessionHandle(5);
+
+        mUwbServiceImpl.closeRanging(sessionHandle);
+
+        verify(mUwbServiceCore).closeRanging(sessionHandle);
+    }
+
+    @Test
+    public void testThrowSecurityExceptionWhenCalledWithoutUwbPrivilegedPermission()
+            throws Exception {
+        doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+                eq(UWB_PRIVILEGED), any());
+        final IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class);
+        try {
+            mUwbServiceImpl.registerAdapterStateCallbacks(cb);
+            fail();
+        } catch (SecurityException e) { /* pass */ }
+    }
+
+    @Test
+    public void testThrowSecurityExceptionWhenSetUwbEnabledCalledWithoutUwbPrivilegedPermission()
+            throws Exception {
+        doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+                eq(UWB_PRIVILEGED), any());
+        try {
+            mUwbServiceImpl.setEnabled(true);
+            fail();
+        } catch (SecurityException e) { /* pass */ }
+    }
+
+    @Test
+    public void testThrowSecurityExceptionWhenOpenRangingCalledWithoutUwbRangingPermission()
+            throws Exception {
+        doThrow(new SecurityException()).when(mUwbInjector).enforceUwbRangingPermissionForPreflight(
+                any());
+
+        final SessionHandle sessionHandle = new SessionHandle(5);
+        final IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class);
+        final PersistableBundle parameters = new PersistableBundle();
+        final IBinder cbBinder = mock(IBinder.class);
+        when(cb.asBinder()).thenReturn(cbBinder);
+        try {
+            mUwbServiceImpl.openRanging(
+                    ATTRIBUTION_SOURCE, sessionHandle, cb, parameters, /* chipId= */ null);
+            fail();
+        } catch (SecurityException e) { /* pass */ }
+    }
+
+    @Test
+    public void testToggleStatePersistenceToSharedPrefs() throws Exception {
+        mUwbServiceImpl.setEnabled(true);
+        verify(mUwbSettingsStore).put(SETTINGS_TOGGLE_STATE, true);
+        verify(mUwbServiceCore).setEnabled(true);
+
+        when(mUwbSettingsStore.get(SETTINGS_TOGGLE_STATE)).thenReturn(false);
+        mUwbServiceImpl.setEnabled(false);
+        verify(mUwbSettingsStore).put(SETTINGS_TOGGLE_STATE, false);
+        verify(mUwbServiceCore).setEnabled(false);
+    }
+
+    @Test
+    public void testToggleStatePersistenceToSharedPrefsWhenApmModeOn() throws Exception {
+        when(mUwbInjector.getSettingsInt(Settings.Global.AIRPLANE_MODE_ON, 0)).thenReturn(1);
+
+        mUwbServiceImpl.setEnabled(true);
+        verify(mUwbSettingsStore).put(SETTINGS_TOGGLE_STATE, true);
+        verify(mUwbServiceCore).setEnabled(false);
+
+        when(mUwbSettingsStore.get(SETTINGS_TOGGLE_STATE)).thenReturn(false);
+        mUwbServiceImpl.setEnabled(false);
+        verify(mUwbSettingsStore).put(SETTINGS_TOGGLE_STATE, false);
+        verify(mUwbServiceCore, times(2)).setEnabled(false);
+    }
+
+    @Test
+    public void testToggleStateReadFromSharedPrefsOnInitialization() throws Exception {
+        when(mUwbServiceCore.getAdapterState()).thenReturn(STATE_ENABLED_ACTIVE);
+        assertThat(mUwbServiceImpl.getAdapterState()).isEqualTo(STATE_ENABLED_ACTIVE);
+        verify(mUwbServiceCore).getAdapterState();
+
+        when(mUwbServiceCore.getAdapterState()).thenReturn(STATE_ENABLED_INACTIVE);
+        assertThat(mUwbServiceImpl.getAdapterState()).isEqualTo(STATE_ENABLED_INACTIVE);
+        verify(mUwbServiceCore, times(2)).getAdapterState();
+    }
+
+    @Test
+    public void testApmModeToggle() throws Exception {
+        mUwbServiceImpl.setEnabled(true);
+        verify(mUwbSettingsStore).put(SETTINGS_TOGGLE_STATE, true);
+        verify(mUwbServiceCore).setEnabled(true);
+
+        // Toggle on
+        when(mUwbInjector.getSettingsInt(Settings.Global.AIRPLANE_MODE_ON, 0)).thenReturn(1);
+        mApmModeBroadcastReceiver.getValue().onReceive(
+                mContext, new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED));
+        verify(mUwbServiceCore).setEnabled(false);
+
+        // Toggle off
+        when(mUwbInjector.getSettingsInt(Settings.Global.AIRPLANE_MODE_ON, 0)).thenReturn(0);
+        mApmModeBroadcastReceiver.getValue().onReceive(
+                mContext, new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED));
+        verify(mUwbServiceCore, times(2)).setEnabled(true);
+    }
+
+    @Test
+    public void testToggleFromRootedShellWhenApmModeOn() throws Exception {
+        BinderUtil.setUid(Process.ROOT_UID);
+        when(mUwbInjector.getSettingsInt(Settings.Global.AIRPLANE_MODE_ON, 0)).thenReturn(1);
+
+        mUwbServiceImpl.setEnabled(true);
+        verify(mUwbSettingsStore).put(SETTINGS_TOGGLE_STATE, true);
+        verify(mUwbServiceCore).setEnabled(true);
+
+        when(mUwbSettingsStore.get(SETTINGS_TOGGLE_STATE)).thenReturn(false);
+        mUwbServiceImpl.setEnabled(false);
+        verify(mUwbSettingsStore).put(SETTINGS_TOGGLE_STATE, false);
+        verify(mUwbServiceCore).setEnabled(false);
+    }
+
+    @Test
+    public void testGetDefaultChipId() {
+        assertEquals(DEFAULT_CHIP_ID, mUwbServiceImpl.getDefaultChipId());
+    }
+
+    @Test
+    public void testThrowSecurityExceptionWhenGetDefaultChipIdWithoutUwbPrivilegedPermission()
+            throws Exception {
+        doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+                eq(UWB_PRIVILEGED), any());
+        try {
+            mUwbServiceImpl.getDefaultChipId();
+            fail();
+        } catch (SecurityException e) { /* pass */ }
+    }
+
+    @Test
+    public void testGetChipIds() {
+        List<String> chipIds = mUwbServiceImpl.getChipIds();
+        assertThat(chipIds).containsExactly(DEFAULT_CHIP_ID);
+    }
+
+    @Test
+    public void testThrowSecurityExceptionWhenGetChipIdsWithoutUwbPrivilegedPermission()
+            throws Exception {
+        doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+                eq(UWB_PRIVILEGED), any());
+        try {
+            mUwbServiceImpl.getChipIds();
+            fail();
+        } catch (SecurityException e) { /* pass */ }
+    }
+
+    @Test
+    public void testGetChipInfos() {
+        List<PersistableBundle> chipInfos = mUwbServiceImpl.getChipInfos();
+        assertThat(chipInfos).hasSize(1);
+        ChipInfoParams chipInfoParams = ChipInfoParams.fromBundle(chipInfos.get(0));
+        assertThat(chipInfoParams.getChipId()).isEqualTo(DEFAULT_CHIP_ID);
+        assertThat(chipInfoParams.getPositionX()).isEqualTo(0.);
+        assertThat(chipInfoParams.getPositionY()).isEqualTo(0.);
+        assertThat(chipInfoParams.getPositionZ()).isEqualTo(0.);
+    }
+
+    @Test
+    public void testThrowSecurityExceptionWhenGetChipInfosWithoutUwbPrivilegedPermission()
+            throws Exception {
+        doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+                eq(UWB_PRIVILEGED), any());
+        try {
+            mUwbServiceImpl.getChipInfos();
+            fail();
+        } catch (SecurityException e) { /* pass */ }
+    }
+
+    @Test
+    public void testAddControlee() throws Exception {
+        final SessionHandle sessionHandle = new SessionHandle(5);
+        final PersistableBundle parameters = new PersistableBundle();
+
+        mUwbServiceImpl.addControlee(sessionHandle, parameters);
+        verify(mUwbServiceCore).addControlee(sessionHandle, parameters);
+    }
+
+    @Test
+    public void testRemoveControlee() throws Exception {
+        final SessionHandle sessionHandle = new SessionHandle(5);
+        final PersistableBundle parameters = new PersistableBundle();
+
+        mUwbServiceImpl.removeControlee(sessionHandle, parameters);
+        verify(mUwbServiceCore).removeControlee(sessionHandle, parameters);
+    }
+
+    @Test
+    public void testAddServiceProfile() throws Exception {
+        final PersistableBundle parameters = new PersistableBundle();
+
+        try {
+            mUwbServiceImpl.addServiceProfile(parameters);
+            fail();
+        } catch (IllegalStateException e) { /* pass */ }
+    }
+
+    @Test
+    public void testGetAdfCertificateAndInfo() throws Exception {
+        final PersistableBundle parameters = new PersistableBundle();
+
+        try {
+            mUwbServiceImpl.getAdfCertificateAndInfo(parameters);
+            fail();
+        } catch (IllegalStateException e) { /* pass */ }
+    }
+
+    @Test
+    public void testGetAdfProvisioningAuthorities() throws Exception {
+        final PersistableBundle parameters = new PersistableBundle();
+
+        try {
+            mUwbServiceImpl.getAdfProvisioningAuthorities(parameters);
+            fail();
+        } catch (IllegalStateException e) { /* pass */ }
+    }
+
+    @Test
+    public void testGetAllServiceProfiles() throws Exception {
+        try {
+            mUwbServiceImpl.getAllServiceProfiles();
+            fail();
+        } catch (IllegalStateException e) { /* pass */ }
+    }
+
+    @Test
+    public void testProvisionProfileAdfByScript() throws Exception {
+        final PersistableBundle parameters = new PersistableBundle();
+        final IUwbAdfProvisionStateCallbacks cb = mock(IUwbAdfProvisionStateCallbacks.class);
+
+        try {
+            mUwbServiceImpl.provisionProfileAdfByScript(parameters, cb);
+            fail();
+        } catch (IllegalStateException e) { /* pass */ }
+    }
+
+    @Test
+    public void testRegisterVendorExtensionCallback() throws Exception {
+        final IUwbVendorUciCallback cb = mock(IUwbVendorUciCallback.class);
+        mUwbServiceImpl.registerVendorExtensionCallback(cb);
+        verify(mUwbServiceCore).registerVendorExtensionCallback(cb);
+    }
+
+    @Test
+    public void testUnregisterVendorExtensionCallback() throws Exception {
+        final IUwbVendorUciCallback cb = mock(IUwbVendorUciCallback.class);
+        mUwbServiceImpl.unregisterVendorExtensionCallback(cb);
+        verify(mUwbServiceCore).unregisterVendorExtensionCallback(cb);
+    }
+
+    @Test
+    public void testRemoveProfileAdf() throws Exception {
+        final PersistableBundle parameters = new PersistableBundle();
+
+        try {
+            mUwbServiceImpl.removeProfileAdf(parameters);
+            fail();
+        } catch (IllegalStateException e) { /* pass */ }
+    }
+
+    @Test
+    public void testRemoveServiceProfile() throws Exception {
+        final PersistableBundle parameters = new PersistableBundle();
+
+        try {
+            mUwbServiceImpl.removeServiceProfile(parameters);
+            fail();
+        } catch (IllegalStateException e) { /* pass */ }
+    }
+
+    @Test
+    public void testResume() throws Exception {
+        final SessionHandle sessionHandle = new SessionHandle(5);
+        final PersistableBundle parameters = new PersistableBundle();
+
+        try {
+            mUwbServiceImpl.resume(sessionHandle, parameters);
+            fail();
+        } catch (IllegalStateException e) { /* pass */ }
+    }
+
+    @Test
+    public void testPause() throws Exception {
+        final SessionHandle sessionHandle = new SessionHandle(5);
+        final PersistableBundle parameters = new PersistableBundle();
+
+        try {
+            mUwbServiceImpl.pause(sessionHandle, parameters);
+            fail();
+        } catch (IllegalStateException e) { /* pass */ }
+    }
+
+    @Test
+    public void testSendData() throws Exception {
+        final SessionHandle sessionHandle = new SessionHandle(5);
+        final UwbAddress mUwbAddress = mock(UwbAddress.class);
+        final PersistableBundle parameters = new PersistableBundle();
+
+        try {
+            mUwbServiceImpl.sendData(sessionHandle, mUwbAddress, parameters, null);
+            fail();
+        } catch (IllegalStateException e) { /* pass */ }
+    }
+
+    @Test
+    public void testSendVendorUciMessage() throws Exception {
+        final int gid = 0;
+        final int oid = 0;
+        mUwbServiceImpl.sendVendorUciMessage(gid, oid, null);
+        verify(mUwbServiceCore).sendVendorUciMessage(gid, oid, null);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java b/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java
new file mode 100644
index 0000000..cb0ca82
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java
@@ -0,0 +1,1444 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyByte;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.content.AttributionSource;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+import android.uwb.IUwbRangingCallbacks;
+import android.uwb.RangingChangeReason;
+import android.uwb.SessionHandle;
+import android.uwb.UwbAddress;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.server.uwb.UwbSessionManager.UwbSession;
+import com.android.server.uwb.UwbSessionManager.WaitObj;
+import com.android.server.uwb.data.UwbMulticastListUpdateStatus;
+import com.android.server.uwb.data.UwbRangingData;
+import com.android.server.uwb.data.UwbUciConstants;
+import com.android.server.uwb.jni.NativeUwbManager;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.ccc.CccOpenRangingParams;
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.ccc.CccPulseShapeCombo;
+import com.google.uwb.support.ccc.CccStartRangingParams;
+import com.google.uwb.support.fira.FiraOpenSessionParams;
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraProtocolVersion;
+import com.google.uwb.support.fira.FiraRangingReconfigureParams;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+
+public class UwbSessionManagerTest {
+    private static final int TEST_SESSION_ID = 7;
+    private static final int MAX_SESSION_NUM = 8;
+    private static final int UID = 343453;
+    private static final String PACKAGE_NAME = "com.uwb.test";
+    private static final AttributionSource ATTRIBUTION_SOURCE =
+            new AttributionSource.Builder(UID).setPackageName(PACKAGE_NAME).build();
+
+    @Mock
+    private UwbConfigurationManager mUwbConfigurationManager;
+    @Mock
+    private NativeUwbManager mNativeUwbManager;
+    @Mock
+    private UwbMetrics mUwbMetrics;
+    @Mock
+    private UwbSessionNotificationManager mUwbSessionNotificationManager;
+    @Mock
+    private UwbInjector mUwbInjector;
+    @Mock
+    private ExecutorService mExecutorService;
+    @Mock
+    private AlarmManager mAlarmManager;
+    private TestLooper mTestLooper = new TestLooper();
+    private UwbSessionManager mUwbSessionManager;
+    private MockitoSession mMockitoSession;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mNativeUwbManager.getMaxSessionNumber()).thenReturn(MAX_SESSION_NUM);
+
+        // TODO: Don't use spy.
+        mUwbSessionManager = spy(new UwbSessionManager(
+                mUwbConfigurationManager,
+                mNativeUwbManager,
+                mUwbMetrics,
+                mUwbSessionNotificationManager,
+                mUwbInjector,
+                mAlarmManager,
+                mTestLooper.getLooper()));
+
+        // static mocking for executor service.
+        mMockitoSession = ExtendedMockito.mockitoSession()
+                .mockStatic(Executors.class, Mockito.withSettings().lenient())
+                .strictness(Strictness.LENIENT)
+                .startMocking();
+
+        doAnswer(invocation -> {
+            FutureTask t = invocation.getArgument(0);
+            t.run();
+            return t;
+        }).when(mExecutorService).submit(any(Runnable.class));
+        when(Executors.newSingleThreadExecutor()).thenReturn(mExecutorService);
+    }
+
+    /**
+     * Called after each test
+     */
+    @After
+    public void cleanup() {
+        if (mMockitoSession != null) {
+            mMockitoSession.finishMocking();
+        }
+    }
+
+    @Test
+    public void onRangeDataNotificationReceivedWithValidUwbSession() {
+        UwbRangingData uwbRangingData =
+                UwbTestUtils.generateRangingData(UwbUciConstants.STATUS_CODE_OK);
+        UwbSession mockUwbSession = mock(UwbSession.class);
+        when(mockUwbSession.getWaitObj()).thenReturn(mock(WaitObj.class));
+        doReturn(mockUwbSession)
+                .when(mUwbSessionManager).getUwbSession(eq(TEST_SESSION_ID));
+
+        mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData);
+
+        verify(mUwbSessionNotificationManager)
+                .onRangingResult(eq(mockUwbSession), eq(uwbRangingData));
+        verify(mUwbMetrics).logRangingResult(anyInt(), eq(uwbRangingData));
+    }
+
+    @Test
+    public void onRangeDataNotificationReceivedWithInvalidSession() {
+        UwbRangingData uwbRangingData =
+                UwbTestUtils.generateRangingData(UwbUciConstants.STATUS_CODE_OK);
+        doReturn(null)
+                .when(mUwbSessionManager).getUwbSession(eq(TEST_SESSION_ID));
+
+        mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData);
+
+        verify(mUwbSessionNotificationManager, never())
+                .onRangingResult(any(), eq(uwbRangingData));
+        verify(mUwbMetrics, never()).logRangingResult(anyInt(), eq(uwbRangingData));
+    }
+
+    @Test
+    public void onMulticastListUpdateNotificationReceivedWithValidSession() {
+        UwbMulticastListUpdateStatus mockUwbMulticastListUpdateStatus =
+                mock(UwbMulticastListUpdateStatus.class);
+        UwbSession mockUwbSession = mock(UwbSession.class);
+        when(mockUwbSession.getWaitObj()).thenReturn(mock(WaitObj.class));
+        doReturn(mockUwbSession)
+                .when(mUwbSessionManager).getUwbSession(anyInt());
+
+        mUwbSessionManager.onMulticastListUpdateNotificationReceived(
+                mockUwbMulticastListUpdateStatus);
+
+        verify(mockUwbSession, times(2)).getWaitObj();
+        verify(mockUwbSession)
+                .setMulticastListUpdateStatus(eq(mockUwbMulticastListUpdateStatus));
+    }
+
+    @Test
+    public void onSessionStatusNotificationReceived_max_retry() {
+        UwbSession mockUwbSession = mock(UwbSession.class);
+        mUwbSessionManager.mSessionTable.put(TEST_SESSION_ID, mockUwbSession);
+        when(mockUwbSession.getWaitObj()).thenReturn(mock(WaitObj.class));
+        when(mockUwbSession.getSessionState()).thenReturn(UwbUciConstants.UWB_SESSION_STATE_ACTIVE);
+
+        mUwbSessionManager.onSessionStatusNotificationReceived(
+                TEST_SESSION_ID,
+                UwbUciConstants.UWB_SESSION_STATE_IDLE,
+                UwbUciConstants.REASON_MAX_RANGING_ROUND_RETRY_COUNT_REACHED);
+
+        verify(mockUwbSession, times(2)).getWaitObj();
+        verify(mockUwbSession).setSessionState(eq(UwbUciConstants.UWB_SESSION_STATE_IDLE));
+        verify(mUwbSessionNotificationManager).onRangingStoppedWithUciReasonCode(
+                eq(mockUwbSession),
+                eq(UwbUciConstants.REASON_MAX_RANGING_ROUND_RETRY_COUNT_REACHED));
+    }
+
+    @Test
+    public void onSessionStatusNotificationReceived_session_mgmt_cmds() {
+        UwbSession mockUwbSession = mock(UwbSession.class);
+        mUwbSessionManager.mSessionTable.put(TEST_SESSION_ID, mockUwbSession);
+        when(mockUwbSession.getWaitObj()).thenReturn(mock(WaitObj.class));
+        when(mockUwbSession.getSessionState()).thenReturn(UwbUciConstants.UWB_SESSION_STATE_ACTIVE);
+
+        mUwbSessionManager.onSessionStatusNotificationReceived(
+                TEST_SESSION_ID,
+                UwbUciConstants.UWB_SESSION_STATE_IDLE,
+                UwbUciConstants.REASON_STATE_CHANGE_WITH_SESSION_MANAGEMENT_COMMANDS);
+
+        verify(mockUwbSession, times(2)).getWaitObj();
+        verify(mockUwbSession).setSessionState(eq(UwbUciConstants.UWB_SESSION_STATE_IDLE));
+        verify(mUwbSessionNotificationManager, never()).onRangingStoppedWithUciReasonCode(
+                any(), anyInt());
+    }
+
+    @Test
+    public void initSession_ExistedSession() throws RemoteException {
+        IUwbRangingCallbacks mockRangingCallbacks = mock(IUwbRangingCallbacks.class);
+        doReturn(true).when(mUwbSessionManager).isExistedSession(anyInt());
+
+        mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, mock(SessionHandle.class),
+                TEST_SESSION_ID, "any", mock(Params.class), mockRangingCallbacks);
+
+        verify(mockRangingCallbacks).onRangingOpenFailed(
+                any(), eq(RangingChangeReason.BAD_PARAMETERS), any());
+        assertThat(mTestLooper.nextMessage()).isNull();
+    }
+
+    @Test
+    public void initSession_maxSession() throws RemoteException {
+        doReturn(MAX_SESSION_NUM).when(mUwbSessionManager).getSessionCount();
+        doReturn(false).when(mUwbSessionManager).isExistedSession(anyInt());
+        IUwbRangingCallbacks mockRangingCallbacks = mock(IUwbRangingCallbacks.class);
+
+        mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, mock(SessionHandle.class),
+                TEST_SESSION_ID, "any", mock(Params.class), mockRangingCallbacks);
+
+        verify(mockRangingCallbacks).onRangingOpenFailed(any(), anyInt(), any());
+        assertThat(mTestLooper.nextMessage()).isNull();
+    }
+
+    @Test
+    public void initSession_UwbSession_RemoteException() throws RemoteException {
+        doReturn(0).when(mUwbSessionManager).getSessionCount();
+        doReturn(false).when(mUwbSessionManager).isExistedSession(anyInt());
+        IUwbRangingCallbacks mockRangingCallbacks = mock(IUwbRangingCallbacks.class);
+        SessionHandle mockSessionHandle = mock(SessionHandle.class);
+        Params mockParams = mock(FiraParams.class);
+        IBinder mockBinder = mock(IBinder.class);
+        UwbSession uwbSession = spy(
+                mUwbSessionManager.new UwbSession(ATTRIBUTION_SOURCE, mockSessionHandle,
+                        TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, mockParams,
+                        mockRangingCallbacks));
+        doReturn(mockBinder).when(uwbSession).getBinder();
+        doReturn(uwbSession).when(mUwbSessionManager).createUwbSession(any(), any(), anyInt(),
+                anyString(), any(), any());
+        doThrow(new RemoteException()).when(mockBinder).linkToDeath(any(), anyInt());
+
+        mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, mockSessionHandle, TEST_SESSION_ID,
+                FiraParams.PROTOCOL_NAME, mockParams, mockRangingCallbacks);
+
+        verify(uwbSession).binderDied();
+        verify(mockRangingCallbacks).onRangingOpenFailed(any(), anyInt(), any());
+        verify(mockBinder, atLeast(1)).unlinkToDeath(any(), anyInt());
+        assertThat(mUwbSessionManager.getSessionCount()).isEqualTo(0);
+        assertThat(mTestLooper.nextMessage()).isNull();
+    }
+
+    @Test
+    public void initSession_success() throws RemoteException {
+        doReturn(0).when(mUwbSessionManager).getSessionCount();
+        doReturn(false).when(mUwbSessionManager).isExistedSession(anyInt());
+        IUwbRangingCallbacks mockRangingCallbacks = mock(IUwbRangingCallbacks.class);
+        SessionHandle mockSessionHandle = mock(SessionHandle.class);
+        Params mockParams = mock(FiraParams.class);
+        IBinder mockBinder = mock(IBinder.class);
+        UwbSession uwbSession = spy(
+                mUwbSessionManager.new UwbSession(ATTRIBUTION_SOURCE, mockSessionHandle,
+                        TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, mockParams,
+                        mockRangingCallbacks));
+        doReturn(mockBinder).when(uwbSession).getBinder();
+        doReturn(uwbSession).when(mUwbSessionManager).createUwbSession(any(), any(), anyInt(),
+                anyString(), any(), any());
+
+        mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, mockSessionHandle, TEST_SESSION_ID,
+                FiraParams.PROTOCOL_NAME, mockParams, mockRangingCallbacks);
+
+        verify(uwbSession, never()).binderDied();
+        verify(mockRangingCallbacks, never()).onRangingOpenFailed(any(), anyInt(), any());
+        verify(mockBinder, never()).unlinkToDeath(any(), anyInt());
+        assertThat(mUwbSessionManager.getUwbSession(TEST_SESSION_ID)).isEqualTo(uwbSession);
+        assertThat(mTestLooper.nextMessage().what).isEqualTo(1); // SESSION_OPEN_RANGING
+    }
+
+    @Test
+    public void deInitSession_notExistedSession() {
+        doReturn(false).when(mUwbSessionManager).isExistedSession(any());
+
+        mUwbSessionManager.deInitSession(mock(SessionHandle.class));
+
+        verify(mUwbSessionManager, never()).getSessionId(any());
+        assertThat(mTestLooper.nextMessage()).isNull();
+    }
+
+    @Test
+    public void deInitSession_success() {
+        doReturn(true).when(mUwbSessionManager).isExistedSession(any());
+        doReturn(TEST_SESSION_ID).when(mUwbSessionManager).getSessionId(any());
+
+        mUwbSessionManager.deInitSession(mock(SessionHandle.class));
+
+        verify(mUwbSessionManager).getUwbSession(eq(TEST_SESSION_ID));
+        assertThat(mTestLooper.nextMessage().what).isEqualTo(5); // SESSION_CLOSE
+    }
+
+    @Test
+    public void startRanging_notExistedSession() {
+        doReturn(false).when(mUwbSessionManager).isExistedSession(any());
+        doReturn(TEST_SESSION_ID).when(mUwbSessionManager).getSessionId(any());
+        doReturn(mock(UwbSession.class)).when(mUwbSessionManager).getUwbSession(anyInt());
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE)
+                .when(mUwbSessionManager).getCurrentSessionState(anyInt());
+
+        mUwbSessionManager.startRanging(mock(SessionHandle.class), mock(Params.class));
+
+        assertThat(mTestLooper.nextMessage()).isNull();
+    }
+
+    @Test
+    public void startRanging_currentSessionStateIdle() {
+        doReturn(true).when(mUwbSessionManager).isExistedSession(any());
+        doReturn(TEST_SESSION_ID).when(mUwbSessionManager).getSessionId(any());
+        UwbSession uwbSession = mock(UwbSession.class);
+        when(uwbSession.getProtocolName()).thenReturn(FiraParams.PROTOCOL_NAME);
+        doReturn(uwbSession).when(mUwbSessionManager).getUwbSession(anyInt());
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE)
+                .when(mUwbSessionManager).getCurrentSessionState(anyInt());
+
+        mUwbSessionManager.startRanging(mock(SessionHandle.class), mock(Params.class));
+
+        assertThat(mTestLooper.nextMessage().what).isEqualTo(2); // SESSION_START_RANGING
+    }
+
+    @Test
+    public void startRanging_currentSessionStateActive() {
+        doReturn(true).when(mUwbSessionManager).isExistedSession(any());
+        doReturn(TEST_SESSION_ID).when(mUwbSessionManager).getSessionId(any());
+        UwbSession mockUwbSession = mock(UwbSession.class);
+        doReturn(mockUwbSession).when(mUwbSessionManager).getUwbSession(anyInt());
+        when(mockUwbSession.getProtocolName()).thenReturn(CccParams.PROTOCOL_NAME);
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_ACTIVE)
+                .when(mUwbSessionManager).getCurrentSessionState(anyInt());
+
+        mUwbSessionManager.startRanging(mock(SessionHandle.class), mock(Params.class));
+
+        verify(mUwbSessionNotificationManager).onRangingStartFailed(
+                any(), eq(UwbUciConstants.STATUS_CODE_REJECTED));
+    }
+
+    @Test
+    public void startRanging_currentSessiionStateInvalid() {
+        doReturn(true).when(mUwbSessionManager).isExistedSession(any());
+        doReturn(TEST_SESSION_ID).when(mUwbSessionManager).getSessionId(any());
+        doReturn(mock(UwbSession.class)).when(mUwbSessionManager).getUwbSession(anyInt());
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_ERROR)
+                .when(mUwbSessionManager).getCurrentSessionState(anyInt());
+
+        mUwbSessionManager.startRanging(mock(SessionHandle.class), mock(Params.class));
+
+        verify(mUwbSessionNotificationManager)
+                .onRangingStartFailed(any(), eq(UwbUciConstants.STATUS_CODE_FAILED));
+    }
+
+    @Test
+    public void stopRanging_notExistedSession() {
+        doReturn(false).when(mUwbSessionManager).isExistedSession(any());
+        doReturn(TEST_SESSION_ID).when(mUwbSessionManager).getSessionId(any());
+        doReturn(mock(UwbSession.class)).when(mUwbSessionManager).getUwbSession(anyInt());
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_ACTIVE)
+                .when(mUwbSessionManager).getCurrentSessionState(anyInt());
+
+        mUwbSessionManager.stopRanging(mock(SessionHandle.class));
+
+        assertThat(mTestLooper.nextMessage()).isNull();
+    }
+
+    @Test
+    public void stopRanging_currentSessionStateActive() {
+        doReturn(true).when(mUwbSessionManager).isExistedSession(any());
+        doReturn(TEST_SESSION_ID).when(mUwbSessionManager).getSessionId(any());
+        doReturn(mock(UwbSession.class)).when(mUwbSessionManager).getUwbSession(anyInt());
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_ACTIVE)
+                .when(mUwbSessionManager).getCurrentSessionState(anyInt());
+
+        mUwbSessionManager.stopRanging(mock(SessionHandle.class));
+
+        assertThat(mTestLooper.nextMessage().what).isEqualTo(3); // SESSION_STOP_RANGING
+    }
+
+    @Test
+    public void stopRanging_currentSessionStateIdle() {
+        doReturn(true).when(mUwbSessionManager).isExistedSession(any());
+        doReturn(TEST_SESSION_ID).when(mUwbSessionManager).getSessionId(any());
+        doReturn(mock(UwbSession.class)).when(mUwbSessionManager).getUwbSession(anyInt());
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE)
+                .when(mUwbSessionManager).getCurrentSessionState(anyInt());
+
+        mUwbSessionManager.stopRanging(mock(SessionHandle.class));
+
+        verify(mUwbSessionNotificationManager).onRangingStopped(any(),
+                eq(UwbUciConstants.REASON_STATE_CHANGE_WITH_SESSION_MANAGEMENT_COMMANDS));
+    }
+
+    @Test
+    public void stopRanging_currentSessionStateInvalid() {
+        doReturn(true).when(mUwbSessionManager).isExistedSession(any());
+        doReturn(TEST_SESSION_ID).when(mUwbSessionManager).getSessionId(any());
+        doReturn(mock(UwbSession.class)).when(mUwbSessionManager).getUwbSession(anyInt());
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_ERROR)
+                .when(mUwbSessionManager).getCurrentSessionState(anyInt());
+
+        mUwbSessionManager.stopRanging(mock(SessionHandle.class));
+
+        verify(mUwbSessionNotificationManager).onRangingStopFailed(any(),
+                eq(UwbUciConstants.STATUS_CODE_REJECTED));
+    }
+
+    @Test
+    public void getUwbSession_success() {
+        UwbSession expectedUwbSession = mock(UwbSession.class);
+        mUwbSessionManager.mSessionTable.put(TEST_SESSION_ID, expectedUwbSession);
+
+        UwbSession actualUwbSession = mUwbSessionManager.getUwbSession(TEST_SESSION_ID);
+
+        assertThat(actualUwbSession).isEqualTo(expectedUwbSession);
+    }
+
+    @Test
+    public void getUwbSession_failed() {
+        UwbSession mockUwbSession = mock(UwbSession.class);
+        mUwbSessionManager.mSessionTable.put(TEST_SESSION_ID, mockUwbSession);
+
+        UwbSession actualUwbSession = mUwbSessionManager.getUwbSession(TEST_SESSION_ID - 1);
+
+        assertThat(actualUwbSession).isNull();
+    }
+
+    @Test
+    public void getSessionId_success() {
+        UwbSession mockUwbSession = mock(UwbSession.class);
+        mUwbSessionManager.mSessionTable.put(TEST_SESSION_ID, mockUwbSession);
+        SessionHandle mockSessionHandle = mock(SessionHandle.class);
+        when(mockUwbSession.getSessionHandle()).thenReturn(mockSessionHandle);
+
+        int actualSessionId = mUwbSessionManager.getSessionId(mockSessionHandle);
+
+        assertThat(actualSessionId).isEqualTo(TEST_SESSION_ID);
+    }
+
+    @Test
+    public void getSessionId_failed() {
+        UwbSession mockUwbSession = mock(UwbSession.class);
+        mUwbSessionManager.mSessionTable.put(TEST_SESSION_ID, mockUwbSession);
+        SessionHandle mockSessionHandle = mock(SessionHandle.class);
+        when(mockUwbSession.getSessionHandle()).thenReturn(mockSessionHandle);
+
+        Integer actualSessionId = mUwbSessionManager.getSessionId(mock(SessionHandle.class));
+
+        assertThat(actualSessionId).isNull();
+    }
+
+    @Test
+    public void isExistedSession_sessionHandle_success() {
+        doReturn(TEST_SESSION_ID).when(mUwbSessionManager).getSessionId(any());
+
+        boolean result = mUwbSessionManager.isExistedSession(mock(SessionHandle.class));
+
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void iexExistedSession_sessionHandle_failed() {
+        doReturn(null).when(mUwbSessionManager).getSessionId(any());
+
+        boolean result = mUwbSessionManager.isExistedSession(mock(SessionHandle.class));
+
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void isExistedSession_sessionId_success() {
+        mUwbSessionManager.mSessionTable.put(TEST_SESSION_ID, mock(UwbSession.class));
+
+        boolean result = mUwbSessionManager.isExistedSession(TEST_SESSION_ID);
+
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void iexExistedSession_sessionId_failed() {
+        boolean result = mUwbSessionManager.isExistedSession(TEST_SESSION_ID);
+
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void stopAllRanging() {
+        UwbSession mockUwbSession1 = mock(UwbSession.class);
+        mUwbSessionManager.mSessionTable.put(TEST_SESSION_ID, mockUwbSession1);
+        UwbSession mockUwbSession2 = mock(UwbSession.class);
+        mUwbSessionManager.mSessionTable.put(TEST_SESSION_ID + 100, mockUwbSession2);
+        when(mNativeUwbManager.stopRanging(eq(TEST_SESSION_ID)))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_FAILED);
+        when(mNativeUwbManager.stopRanging(eq(TEST_SESSION_ID + 100)))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+
+        mUwbSessionManager.stopAllRanging();
+
+        verify(mNativeUwbManager, times(2)).stopRanging(anyInt());
+        verify(mockUwbSession1, never()).setSessionState(anyInt());
+        verify(mockUwbSession2).setSessionState(eq(UwbUciConstants.UWB_SESSION_STATE_IDLE));
+    }
+
+    @Test
+    public void deinitAllSession() {
+        UwbSession mockUwbSession1 = mock(UwbSession.class);
+        mUwbSessionManager.mSessionTable.put(TEST_SESSION_ID, mockUwbSession1);
+        when(mockUwbSession1.getBinder()).thenReturn(mock(IBinder.class));
+        when(mockUwbSession1.getSessionId()).thenReturn(TEST_SESSION_ID);
+        UwbSession mockUwbSession2 = mock(UwbSession.class);
+        mUwbSessionManager.mSessionTable.put(TEST_SESSION_ID + 100, mockUwbSession2);
+        when(mockUwbSession2.getBinder()).thenReturn(mock(IBinder.class));
+        when(mockUwbSession2.getSessionId()).thenReturn(TEST_SESSION_ID + 100);
+
+        mUwbSessionManager.deinitAllSession();
+
+        verify(mUwbSessionNotificationManager, times(2))
+                .onRangingClosedWithApiReasonCode(any(), eq(RangingChangeReason.SYSTEM_POLICY));
+        verify(mUwbSessionManager, times(2)).removeSession(any());
+        // TODO: enable it when the resetDevice is enabled.
+        // verify(mNativeUwbManager).resetDevice(eq(UwbUciConstants.UWBS_RESET));
+        assertThat(mUwbSessionManager.getSessionCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void setCurrentSessionState() {
+        UwbSession mockUwbSession = mock(UwbSession.class);
+        mUwbSessionManager.mSessionTable.put(TEST_SESSION_ID, mockUwbSession);
+
+        mUwbSessionManager.setCurrentSessionState(
+                TEST_SESSION_ID, UwbUciConstants.UWB_SESSION_STATE_ACTIVE);
+
+        verify(mockUwbSession).setSessionState(eq(UwbUciConstants.UWB_SESSION_STATE_ACTIVE));
+    }
+
+    @Test
+    public void getCurrentSessionState_nullSession() {
+        int actualStatus = mUwbSessionManager.getCurrentSessionState(TEST_SESSION_ID);
+
+        assertThat(actualStatus).isEqualTo(UwbUciConstants.UWB_SESSION_STATE_ERROR);
+    }
+
+    @Test
+    public void getCurrentSessionState_success() {
+        UwbSession mockUwbSession = mock(UwbSession.class);
+        mUwbSessionManager.mSessionTable.put(TEST_SESSION_ID, mockUwbSession);
+        when(mockUwbSession.getSessionState()).thenReturn(UwbUciConstants.UWB_SESSION_STATE_ACTIVE);
+
+        int actualStatus = mUwbSessionManager.getCurrentSessionState(TEST_SESSION_ID);
+
+        assertThat(actualStatus).isEqualTo(UwbUciConstants.UWB_SESSION_STATE_ACTIVE);
+    }
+
+    @Test
+    public void getSessionIdSet() {
+        UwbSession mockUwbSession = mock(UwbSession.class);
+        mUwbSessionManager.mSessionTable.put(TEST_SESSION_ID, mockUwbSession);
+
+        Set<Integer> actualSessionIds = mUwbSessionManager.getSessionIdSet();
+
+        assertThat(actualSessionIds).hasSize(1);
+        assertThat(actualSessionIds.contains(TEST_SESSION_ID)).isTrue();
+    }
+
+    @Test
+    public void reconfigure_notExistedSession() {
+        doReturn(false).when(mUwbSessionManager).isExistedSession(any());
+
+        int actualStatus = mUwbSessionManager.reconfigure(
+                mock(SessionHandle.class), mock(Params.class));
+
+        assertThat(actualStatus).isEqualTo(UwbUciConstants.STATUS_CODE_ERROR_SESSION_NOT_EXIST);
+    }
+
+    @Test
+    public void reconfigure_calledSuccess() {
+        doReturn(true).when(mUwbSessionManager).isExistedSession(any());
+        FiraRangingReconfigureParams params =
+                new FiraRangingReconfigureParams.Builder()
+                        .setBlockStrideLength(10)
+                        .setRangeDataNtfConfig(1)
+                        .setRangeDataProximityFar(10)
+                        .setRangeDataProximityNear(2)
+                        .build();
+
+        int actualStatus = mUwbSessionManager.reconfigure(mock(SessionHandle.class), params);
+
+        assertThat(actualStatus).isEqualTo(0);
+        assertThat(mTestLooper.nextMessage().what)
+                .isEqualTo(4); // SESSION_RECONFIG_RANGING
+    }
+
+    private UwbSession setUpUwbSessionForExecution() throws RemoteException {
+        // setup message
+        doReturn(0).when(mUwbSessionManager).getSessionCount();
+        doReturn(false).when(mUwbSessionManager).isExistedSession(anyInt());
+        IUwbRangingCallbacks mockRangingCallbacks = mock(IUwbRangingCallbacks.class);
+        SessionHandle mockSessionHandle = mock(SessionHandle.class);
+        Params params = new FiraOpenSessionParams.Builder()
+                .setDeviceAddress(UwbAddress.fromBytes(new byte[] {(byte) 0x01, (byte) 0x02 }))
+                .setVendorId(new byte[] { (byte) 0x00, (byte) 0x01 })
+                .setStaticStsIV(new byte[] { (byte) 0x01, (byte) 0x02, (byte) 0x03,
+                        (byte) 0x04, (byte) 0x05, (byte) 0x06 })
+                .setDestAddressList(Arrays.asList(
+                        UwbAddress.fromBytes(new byte[] {(byte) 0x03, (byte) 0x04 })))
+                .setProtocolVersion(new FiraProtocolVersion(1, 0))
+                .setSessionId(10)
+                .setDeviceType(FiraParams.RANGING_DEVICE_TYPE_CONTROLLER)
+                .setDeviceRole(FiraParams.RANGING_DEVICE_ROLE_INITIATOR)
+                .setMultiNodeMode(FiraParams.MULTI_NODE_MODE_UNICAST)
+                .build();
+        IBinder mockBinder = mock(IBinder.class);
+        UwbSession uwbSession = spy(
+                mUwbSessionManager.new UwbSession(ATTRIBUTION_SOURCE, mockSessionHandle,
+                        TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, params, mockRangingCallbacks));
+        doReturn(mockBinder).when(uwbSession).getBinder();
+        doReturn(uwbSession).when(mUwbSessionManager).createUwbSession(any(), any(), anyInt(),
+                anyString(), any(), any());
+        doReturn(mock(WaitObj.class)).when(uwbSession).getWaitObj();
+
+        return uwbSession;
+    }
+
+    private UwbSession setUpCccUwbSessionForExecution() throws RemoteException {
+        // setup message
+        doReturn(0).when(mUwbSessionManager).getSessionCount();
+        doReturn(false).when(mUwbSessionManager).isExistedSession(anyInt());
+        IUwbRangingCallbacks mockRangingCallbacks = mock(IUwbRangingCallbacks.class);
+        SessionHandle mockSessionHandle = mock(SessionHandle.class);
+        Params params = new CccOpenRangingParams.Builder()
+                .setProtocolVersion(CccParams.PROTOCOL_VERSION_1_0)
+                .setUwbConfig(CccParams.UWB_CONFIG_0)
+                .setPulseShapeCombo(
+                        new CccPulseShapeCombo(
+                                CccParams.PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE,
+                                CccParams.PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE))
+                .setSessionId(1)
+                .setRanMultiplier(4)
+                .setChannel(CccParams.UWB_CHANNEL_9)
+                .setNumChapsPerSlot(CccParams.CHAPS_PER_SLOT_3)
+                .setNumResponderNodes(1)
+                .setNumSlotsPerRound(CccParams.SLOTS_PER_ROUND_6)
+                .setSyncCodeIndex(1)
+                .setHoppingConfigMode(CccParams.HOPPING_CONFIG_MODE_NONE)
+                .setHoppingSequence(CccParams.HOPPING_SEQUENCE_DEFAULT)
+                .build();
+        IBinder mockBinder = mock(IBinder.class);
+        UwbSession uwbSession = spy(
+                mUwbSessionManager.new UwbSession(ATTRIBUTION_SOURCE, mockSessionHandle,
+                        TEST_SESSION_ID, CccParams.PROTOCOL_NAME, params, mockRangingCallbacks));
+        doReturn(mockBinder).when(uwbSession).getBinder();
+        doReturn(uwbSession).when(mUwbSessionManager).createUwbSession(any(), any(), anyInt(),
+                anyString(), any(), any());
+        doReturn(mock(WaitObj.class)).when(uwbSession).getWaitObj();
+
+        return uwbSession;
+    }
+
+    @Test
+    public void openRanging_success() throws Exception {
+        UwbSession uwbSession = setUpUwbSessionForExecution();
+        // stub for openRanging conditions
+        when(mNativeUwbManager.initSession(anyInt(), anyByte()))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_INIT,
+                UwbUciConstants.UWB_SESSION_STATE_IDLE).when(uwbSession).getSessionState();
+        when(mUwbConfigurationManager.setAppConfigurations(anyInt(), any()))
+                .thenReturn(UwbUciConstants.STATUS_CODE_OK);
+
+
+        mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(),
+                TEST_SESSION_ID, FiraParams.PROTOCOL_NAME,
+                uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks());
+        mTestLooper.dispatchAll();
+
+        verify(mNativeUwbManager).initSession(eq(TEST_SESSION_ID), anyByte());
+        verify(mUwbConfigurationManager).setAppConfigurations(eq(TEST_SESSION_ID), any());
+        verify(mUwbSessionNotificationManager).onRangingOpened(eq(uwbSession));
+        verify(mUwbMetrics).logRangingInitEvent(eq(uwbSession),
+                eq(UwbUciConstants.STATUS_CODE_OK));
+    }
+
+    @Test
+    public void openRanging_timeout() throws Exception {
+        UwbSession uwbSession = setUpUwbSessionForExecution();
+        // stub for openRanging conditions
+        when(mNativeUwbManager.initSession(anyInt(), anyByte()))
+                .thenThrow(new IllegalStateException());
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_INIT,
+                UwbUciConstants.UWB_SESSION_STATE_IDLE).when(uwbSession).getSessionState();
+        when(mUwbConfigurationManager.setAppConfigurations(anyInt(), any()))
+                .thenReturn(UwbUciConstants.STATUS_CODE_OK);
+
+
+        mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(),
+                TEST_SESSION_ID, FiraParams.PROTOCOL_NAME,
+                uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks());
+        mTestLooper.dispatchAll();
+
+        verify(mUwbMetrics).logRangingInitEvent(eq(uwbSession),
+                eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mUwbSessionNotificationManager)
+                .onRangingOpenFailed(eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mNativeUwbManager).deInitSession(eq(TEST_SESSION_ID));
+    }
+
+    @Test
+    public void openRanging_nativeInitSessionFailed() throws Exception {
+        UwbSession uwbSession = setUpUwbSessionForExecution();
+        // stub for openRanging conditions
+        when(mNativeUwbManager.initSession(anyInt(), anyByte()))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_FAILED);
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_INIT,
+                UwbUciConstants.UWB_SESSION_STATE_IDLE).when(uwbSession).getSessionState();
+        when(mUwbConfigurationManager.setAppConfigurations(anyInt(), any()))
+                .thenReturn(UwbUciConstants.STATUS_CODE_OK);
+
+
+        mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(),
+                TEST_SESSION_ID, FiraParams.PROTOCOL_NAME,
+                uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks());
+        mTestLooper.dispatchAll();
+
+        verify(mUwbMetrics).logRangingInitEvent(eq(uwbSession),
+                eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mUwbSessionNotificationManager)
+                .onRangingOpenFailed(eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mNativeUwbManager).deInitSession(eq(TEST_SESSION_ID));
+    }
+
+    @Test
+    public void openRanging_setAppConfigurationFailed() throws Exception {
+        UwbSession uwbSession = setUpUwbSessionForExecution();
+        // stub for openRanging conditions
+        when(mNativeUwbManager.initSession(anyInt(), anyByte()))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_INIT,
+                UwbUciConstants.UWB_SESSION_STATE_IDLE).when(uwbSession).getSessionState();
+        when(mUwbConfigurationManager.setAppConfigurations(anyInt(), any()))
+                .thenReturn(UwbUciConstants.STATUS_CODE_FAILED);
+
+
+        mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(),
+                TEST_SESSION_ID, FiraParams.PROTOCOL_NAME,
+                uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks());
+        mTestLooper.dispatchAll();
+
+        verify(mUwbMetrics).logRangingInitEvent(eq(uwbSession),
+                eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mUwbSessionNotificationManager)
+                .onRangingOpenFailed(eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mNativeUwbManager).deInitSession(eq(TEST_SESSION_ID));
+    }
+
+    @Test
+    public void openRanging_wrongInitState() throws Exception {
+        UwbSession uwbSession = setUpUwbSessionForExecution();
+        // stub for openRanging conditions
+        when(mNativeUwbManager.initSession(anyInt(), anyByte()))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_ERROR,
+                UwbUciConstants.UWB_SESSION_STATE_IDLE).when(uwbSession).getSessionState();
+        when(mUwbConfigurationManager.setAppConfigurations(anyInt(), any()))
+                .thenReturn(UwbUciConstants.STATUS_CODE_FAILED);
+
+
+        mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(),
+                TEST_SESSION_ID, FiraParams.PROTOCOL_NAME,
+                uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks());
+        mTestLooper.dispatchAll();
+
+        verify(mUwbMetrics).logRangingInitEvent(eq(uwbSession),
+                eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mUwbSessionNotificationManager)
+                .onRangingOpenFailed(eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mNativeUwbManager).deInitSession(eq(TEST_SESSION_ID));
+    }
+
+    @Test
+    public void openRanging_wrongIdleState() throws Exception {
+        UwbSession uwbSession = setUpUwbSessionForExecution();
+        // stub for openRanging conditions
+        when(mNativeUwbManager.initSession(anyInt(), anyByte()))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_INIT,
+                UwbUciConstants.UWB_SESSION_STATE_ERROR).when(uwbSession).getSessionState();
+        when(mUwbConfigurationManager.setAppConfigurations(anyInt(), any()))
+                .thenReturn(UwbUciConstants.STATUS_CODE_FAILED);
+
+
+        mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(),
+                TEST_SESSION_ID, FiraParams.PROTOCOL_NAME,
+                uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks());
+        mTestLooper.dispatchAll();
+
+        verify(mUwbMetrics).logRangingInitEvent(eq(uwbSession),
+                eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mUwbSessionNotificationManager)
+                .onRangingOpenFailed(eq(uwbSession),
+                        eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mNativeUwbManager).deInitSession(eq(TEST_SESSION_ID));
+    }
+
+    private UwbSession prepareExistingUwbSession() throws Exception {
+        UwbSession uwbSession = setUpUwbSessionForExecution();
+        mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(),
+                TEST_SESSION_ID, FiraParams.PROTOCOL_NAME,
+                uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks());
+        mTestLooper.nextMessage(); // remove the OPEN_RANGING msg;
+
+        assertThat(mTestLooper.isIdle()).isFalse();
+
+        return uwbSession;
+    }
+
+    private UwbSession prepareExistingCccUwbSession() throws Exception {
+        UwbSession uwbSession = setUpCccUwbSessionForExecution();
+        mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(),
+                TEST_SESSION_ID, CccParams.PROTOCOL_NAME,
+                uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks());
+        mTestLooper.nextMessage(); // remove the OPEN_RANGING msg;
+
+        assertThat(mTestLooper.isIdle()).isFalse();
+
+        return uwbSession;
+    }
+
+    @Test
+    public void startRanging_sessionStateIdle() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        // set up for start ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE)
+                .when(uwbSession).getSessionState();
+
+        mUwbSessionManager.startRanging(
+                uwbSession.getSessionHandle(), uwbSession.getParams());
+
+        assertThat(mTestLooper.isIdle()).isTrue();
+        assertThat(mTestLooper.nextMessage().what).isEqualTo(2); // SESSION_START_RANGING
+    }
+
+    @Test
+    public void startRanging_sessionStateActive() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        // set up for start ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_ACTIVE)
+                .when(uwbSession).getSessionState();
+
+        mUwbSessionManager.startRanging(
+                uwbSession.getSessionHandle(), uwbSession.getParams());
+
+        assertThat(mTestLooper.isIdle()).isFalse();
+        verify(mUwbSessionNotificationManager).onRangingStartFailed(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_REJECTED));
+    }
+
+    @Test
+    public void startRanging_sessionStateError() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        // set up for start ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_ERROR)
+                .when(uwbSession).getSessionState();
+
+        mUwbSessionManager.startRanging(
+                uwbSession.getSessionHandle(), uwbSession.getParams());
+
+        assertThat(mTestLooper.isIdle()).isFalse();
+        verify(mUwbSessionNotificationManager).onRangingStartFailed(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mUwbMetrics).longRangingStartEvent(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+    }
+
+    @Test
+    public void execStartRanging_success() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        // set up for start ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE, UwbUciConstants.UWB_SESSION_STATE_ACTIVE)
+                .when(uwbSession).getSessionState();
+        when(mNativeUwbManager.startRanging(eq(TEST_SESSION_ID)))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+
+        mUwbSessionManager.startRanging(
+                uwbSession.getSessionHandle(), uwbSession.getParams());
+        mTestLooper.dispatchAll();
+
+        verify(mUwbSessionNotificationManager).onRangingStarted(eq(uwbSession), any());
+        verify(mUwbMetrics).longRangingStartEvent(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_OK));
+    }
+
+    @Test
+    public void execStartRanging_onRangeDataNotification() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        // set up for start ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE, UwbUciConstants.UWB_SESSION_STATE_ACTIVE)
+                .when(uwbSession).getSessionState();
+        when(mNativeUwbManager.startRanging(eq(TEST_SESSION_ID)))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+
+        mUwbSessionManager.startRanging(
+                uwbSession.getSessionHandle(), uwbSession.getParams());
+        mTestLooper.dispatchAll();
+
+        verify(mUwbSessionNotificationManager).onRangingStarted(eq(uwbSession), any());
+        verify(mUwbMetrics).longRangingStartEvent(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_OK));
+
+        // Now send a range data notification.
+        UwbRangingData uwbRangingData =
+                UwbTestUtils.generateRangingData(UwbUciConstants.STATUS_CODE_OK);
+        mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData);
+        verify(mUwbSessionNotificationManager).onRangingResult(uwbSession, uwbRangingData);
+    }
+
+    @Test
+    public void execStartRanging_onRangeDataNotificationContinuousErrors() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        // set up for start ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE, UwbUciConstants.UWB_SESSION_STATE_ACTIVE)
+                .when(uwbSession).getSessionState();
+        when(mNativeUwbManager.startRanging(eq(TEST_SESSION_ID)))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+
+        mUwbSessionManager.startRanging(
+                uwbSession.getSessionHandle(), uwbSession.getParams());
+        mTestLooper.dispatchAll();
+
+        verify(mUwbSessionNotificationManager).onRangingStarted(eq(uwbSession), any());
+        verify(mUwbMetrics).longRangingStartEvent(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_OK));
+
+        // Now send a range data notification with an error.
+        UwbRangingData uwbRangingData =
+                UwbTestUtils.generateRangingData(UwbUciConstants.STATUS_CODE_RANGING_RX_TIMEOUT);
+        mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData);
+        verify(mUwbSessionNotificationManager).onRangingResult(uwbSession, uwbRangingData);
+        ArgumentCaptor<AlarmManager.OnAlarmListener> alarmListenerCaptor =
+                ArgumentCaptor.forClass(AlarmManager.OnAlarmListener.class);
+        verify(mAlarmManager).set(
+                anyInt(), anyLong(), anyString(), alarmListenerCaptor.capture(), any());
+        assertThat(alarmListenerCaptor.getValue()).isNotNull();
+
+        // Send one more error and ensure that the timer is not cancelled.
+        uwbRangingData =
+                UwbTestUtils.generateRangingData(UwbUciConstants.STATUS_CODE_RANGING_RX_TIMEOUT);
+        mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData);
+        verify(mUwbSessionNotificationManager).onRangingResult(uwbSession, uwbRangingData);
+
+        verify(mAlarmManager, never()).cancel(any(AlarmManager.OnAlarmListener.class));
+
+        // set up for stop ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_ACTIVE, UwbUciConstants.UWB_SESSION_STATE_IDLE)
+                .when(uwbSession).getSessionState();
+        when(mNativeUwbManager.stopRanging(eq(TEST_SESSION_ID)))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+
+        // Now fire the timer callback.
+        alarmListenerCaptor.getValue().onAlarm();
+
+        // Expect session stop.
+        mTestLooper.dispatchNext();
+        verify(mUwbSessionNotificationManager)
+                .onRangingStopped(eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_OK));
+        verify(mUwbMetrics).longRangingStopEvent(eq(uwbSession));
+    }
+
+    @Test
+    public void execStartRanging_onRangeDataNotificationErrorFollowedBySuccess() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        // set up for start ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE, UwbUciConstants.UWB_SESSION_STATE_ACTIVE)
+                .when(uwbSession).getSessionState();
+        when(mNativeUwbManager.startRanging(eq(TEST_SESSION_ID)))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+
+        mUwbSessionManager.startRanging(
+                uwbSession.getSessionHandle(), uwbSession.getParams());
+        mTestLooper.dispatchAll();
+
+        verify(mUwbSessionNotificationManager).onRangingStarted(eq(uwbSession), any());
+        verify(mUwbMetrics).longRangingStartEvent(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_OK));
+
+        // Now send a range data notification with an error.
+        UwbRangingData uwbRangingData =
+                UwbTestUtils.generateRangingData(UwbUciConstants.STATUS_CODE_RANGING_RX_TIMEOUT);
+        mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData);
+        verify(mUwbSessionNotificationManager).onRangingResult(uwbSession, uwbRangingData);
+        ArgumentCaptor<AlarmManager.OnAlarmListener> alarmListenerCaptor =
+                ArgumentCaptor.forClass(AlarmManager.OnAlarmListener.class);
+        verify(mAlarmManager).set(
+                anyInt(), anyLong(), anyString(), alarmListenerCaptor.capture(), any());
+        assertThat(alarmListenerCaptor.getValue()).isNotNull();
+
+        // Send success and ensure that the timer is cancelled.
+        uwbRangingData =
+                UwbTestUtils.generateRangingData(UwbUciConstants.STATUS_CODE_OK);
+        mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData);
+        verify(mUwbSessionNotificationManager).onRangingResult(uwbSession, uwbRangingData);
+
+        verify(mAlarmManager).cancel(any(AlarmManager.OnAlarmListener.class));
+    }
+
+    @Test
+    public void execStartCccRanging_success() throws Exception {
+        UwbSession uwbSession = prepareExistingCccUwbSession();
+        // set up for start ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE, UwbUciConstants.UWB_SESSION_STATE_ACTIVE)
+                .when(uwbSession).getSessionState();
+        when(mNativeUwbManager.startRanging(eq(TEST_SESSION_ID)))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+        CccStartRangingParams cccStartRangingParams = new CccStartRangingParams.Builder()
+                .setSessionId(TEST_SESSION_ID)
+                .setRanMultiplier(8)
+                .build();
+        mUwbSessionManager.startRanging(
+                uwbSession.getSessionHandle(), cccStartRangingParams);
+        mTestLooper.dispatchAll();
+
+        // Verify the update logic.
+        CccOpenRangingParams cccOpenRangingParams = (CccOpenRangingParams) uwbSession.getParams();
+        assertThat(cccOpenRangingParams.getRanMultiplier()).isEqualTo(8);
+    }
+
+    @Test
+    public void execStartCccRangingWithNoStartParams_success() throws Exception {
+        UwbSession uwbSession = prepareExistingCccUwbSession();
+        // set up for start ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE, UwbUciConstants.UWB_SESSION_STATE_ACTIVE)
+                .when(uwbSession).getSessionState();
+        when(mNativeUwbManager.startRanging(eq(TEST_SESSION_ID)))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+        mUwbSessionManager.startRanging(uwbSession.getSessionHandle(), null /* params */);
+        mTestLooper.dispatchAll();
+
+        // Verify that RAN multiplier from open is used.
+        CccOpenRangingParams cccOpenRangingParams = (CccOpenRangingParams) uwbSession.getParams();
+        assertThat(cccOpenRangingParams.getRanMultiplier()).isEqualTo(4);
+    }
+
+    @Test
+    public void execStartRanging_executionException() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        // set up for start ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE, UwbUciConstants.UWB_SESSION_STATE_ACTIVE)
+                .when(uwbSession).getSessionState();
+        when(mNativeUwbManager.startRanging(eq(TEST_SESSION_ID)))
+                .thenThrow(new IllegalStateException());
+
+        mUwbSessionManager.startRanging(
+                uwbSession.getSessionHandle(), uwbSession.getParams());
+        mTestLooper.dispatchAll();
+
+        verify(mUwbMetrics).longRangingStartEvent(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+    }
+
+    @Test
+    public void execStartRanging_nativeStartRangingFailed() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        // set up for start ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE, UwbUciConstants.UWB_SESSION_STATE_ACTIVE)
+                .when(uwbSession).getSessionState();
+        when(mNativeUwbManager.startRanging(eq(TEST_SESSION_ID)))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_FAILED);
+
+        mUwbSessionManager.startRanging(
+                uwbSession.getSessionHandle(), uwbSession.getParams());
+        mTestLooper.dispatchAll();
+
+        verify(mUwbSessionNotificationManager).onRangingStartFailed(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mUwbMetrics).longRangingStartEvent(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+    }
+
+    @Test
+    public void execStartRanging_wrongSessionState() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        // set up for start ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE, UwbUciConstants.UWB_SESSION_STATE_ERROR)
+                .when(uwbSession).getSessionState();
+        when(mNativeUwbManager.startRanging(eq(TEST_SESSION_ID)))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+
+        mUwbSessionManager.startRanging(
+                uwbSession.getSessionHandle(), uwbSession.getParams());
+        mTestLooper.dispatchAll();
+
+        verify(mUwbSessionNotificationManager).onRangingStartFailed(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mUwbMetrics).longRangingStartEvent(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+    }
+
+    @Test
+    public void stopRanging_sessionStateActive() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        // set up for stop ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_ACTIVE).when(uwbSession).getSessionState();
+
+        mUwbSessionManager.stopRanging(uwbSession.getSessionHandle());
+
+        assertThat(mTestLooper.nextMessage().what).isEqualTo(3); // SESSION_STOP_RANGING
+    }
+
+    @Test
+    public void stopRanging_sessionStateIdle() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        // set up for stop ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE).when(uwbSession).getSessionState();
+
+        mUwbSessionManager.stopRanging(uwbSession.getSessionHandle());
+
+        verify(mUwbSessionNotificationManager).onRangingStopped(
+                eq(uwbSession),
+                eq(UwbUciConstants.REASON_STATE_CHANGE_WITH_SESSION_MANAGEMENT_COMMANDS));
+        verify(mUwbMetrics).longRangingStopEvent(eq(uwbSession));
+    }
+
+    @Test
+    public void stopRanging_sessionStateError() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        // set up for stop ranging
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_ERROR).when(uwbSession).getSessionState();
+
+        mUwbSessionManager.stopRanging(uwbSession.getSessionHandle());
+
+        verify(mUwbSessionNotificationManager).onRangingStopFailed(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_REJECTED));
+    }
+
+    @Test
+    public void execStopRanging_success() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_ACTIVE, UwbUciConstants.UWB_SESSION_STATE_IDLE)
+                .when(uwbSession).getSessionState();
+        when(mNativeUwbManager.stopRanging(eq(TEST_SESSION_ID)))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+
+        mUwbSessionManager.stopRanging(uwbSession.getSessionHandle());
+        mTestLooper.dispatchNext();
+
+        verify(mUwbSessionNotificationManager)
+                .onRangingStopped(eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_OK));
+        verify(mUwbMetrics).longRangingStopEvent(eq(uwbSession));
+    }
+
+    @Test
+    public void execStopRanging_exception() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_ACTIVE, UwbUciConstants.UWB_SESSION_STATE_IDLE)
+                .when(uwbSession).getSessionState();
+        when(mNativeUwbManager.stopRanging(eq(TEST_SESSION_ID)))
+                .thenThrow(new IllegalStateException());
+
+        mUwbSessionManager.stopRanging(uwbSession.getSessionHandle());
+        mTestLooper.dispatchNext();
+
+        verify(mUwbSessionNotificationManager, never()).onRangingStopped(any(), anyInt());
+    }
+
+    @Test
+    public void execStopRanging_nativeFailed() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        doReturn(UwbUciConstants.UWB_SESSION_STATE_ACTIVE, UwbUciConstants.UWB_SESSION_STATE_IDLE)
+                .when(uwbSession).getSessionState();
+        when(mNativeUwbManager.stopRanging(eq(TEST_SESSION_ID)))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_FAILED);
+
+        mUwbSessionManager.stopRanging(uwbSession.getSessionHandle());
+        mTestLooper.dispatchNext();
+
+        verify(mUwbSessionNotificationManager)
+                .onRangingStopFailed(eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mUwbMetrics, never()).longRangingStopEvent(eq(uwbSession));
+    }
+
+    @Test
+    public void reconfigure_notExistingSession() {
+        int status = mUwbSessionManager.reconfigure(mock(SessionHandle.class), mock(Params.class));
+
+        assertThat(status).isEqualTo(UwbUciConstants.STATUS_CODE_ERROR_SESSION_NOT_EXIST);
+    }
+
+    private FiraRangingReconfigureParams buildReconfigureParams() {
+        return buildReconfigureParams(FiraParams.MULTICAST_LIST_UPDATE_ACTION_ADD);
+    }
+
+    private FiraRangingReconfigureParams buildReconfigureParams(int action) {
+        FiraRangingReconfigureParams reconfigureParams =
+                new FiraRangingReconfigureParams.Builder()
+                        .setAddressList(new UwbAddress[] {
+                                UwbAddress.fromBytes(new byte[] { (byte) 0x01, (byte) 0x02 }) })
+                        .setAction(action)
+                        .setSubSessionIdList(new int[] { 2 })
+                        .build();
+
+        return spy(reconfigureParams);
+    }
+
+    @Test
+    public void reconfigure_existingSession() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+
+        int status = mUwbSessionManager.reconfigure(
+                uwbSession.getSessionHandle(), buildReconfigureParams());
+
+        assertThat(status).isEqualTo(0);
+        assertThat(mTestLooper.nextMessage().what).isEqualTo(4); // SESSION_RECONFIGURE_RANGING
+    }
+
+    @Test
+    public void execReconfigureAddControlee_success() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        FiraRangingReconfigureParams reconfigureParams =
+                buildReconfigureParams();
+        when(mNativeUwbManager
+                .controllerMulticastListUpdate(anyInt(), anyInt(), anyInt(), any(), any()))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+        UwbMulticastListUpdateStatus uwbMulticastListUpdateStatus =
+                mock(UwbMulticastListUpdateStatus.class);
+        when(uwbMulticastListUpdateStatus.getNumOfControlee()).thenReturn(1);
+        when(uwbMulticastListUpdateStatus.getStatus()).thenReturn(
+                new int[] { UwbUciConstants.STATUS_CODE_OK });
+        doReturn(uwbMulticastListUpdateStatus).when(uwbSession).getMulticastListUpdateStatus();
+        when(mUwbConfigurationManager.setAppConfigurations(anyInt(), any()))
+                .thenReturn(UwbUciConstants.STATUS_CODE_OK);
+
+        mUwbSessionManager.reconfigure(uwbSession.getSessionHandle(), reconfigureParams);
+        mTestLooper.dispatchNext();
+
+        short dstAddress =
+                ByteBuffer.wrap(reconfigureParams.getAddressList()[0].toBytes()).getShort(0);
+        verify(mNativeUwbManager).controllerMulticastListUpdate(
+                uwbSession.getSessionId(), reconfigureParams.getAction(), 1,
+                new short[] {dstAddress}, reconfigureParams.getSubSessionIdList());
+        verify(mUwbSessionNotificationManager).onControleeAdded(eq(uwbSession));
+        verify(mUwbSessionNotificationManager).onRangingReconfigured(eq(uwbSession));
+    }
+
+    @Test
+    public void execReconfigureRemoveControlee_success() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        FiraRangingReconfigureParams reconfigureParams =
+                buildReconfigureParams(FiraParams.MULTICAST_LIST_UPDATE_ACTION_DELETE);
+        when(mNativeUwbManager
+                .controllerMulticastListUpdate(anyInt(), anyInt(), anyInt(), any(), any()))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+        UwbMulticastListUpdateStatus uwbMulticastListUpdateStatus =
+                mock(UwbMulticastListUpdateStatus.class);
+        when(uwbMulticastListUpdateStatus.getNumOfControlee()).thenReturn(1);
+        when(uwbMulticastListUpdateStatus.getStatus()).thenReturn(
+                new int[] { UwbUciConstants.STATUS_CODE_OK });
+        doReturn(uwbMulticastListUpdateStatus).when(uwbSession).getMulticastListUpdateStatus();
+        when(mUwbConfigurationManager.setAppConfigurations(anyInt(), any()))
+                .thenReturn(UwbUciConstants.STATUS_CODE_OK);
+
+        mUwbSessionManager.reconfigure(uwbSession.getSessionHandle(), reconfigureParams);
+        mTestLooper.dispatchNext();
+
+        short dstAddress =
+                ByteBuffer.wrap(reconfigureParams.getAddressList()[0].toBytes()).getShort(0);
+        verify(mNativeUwbManager).controllerMulticastListUpdate(
+                uwbSession.getSessionId(), reconfigureParams.getAction(), 1,
+                new short[] {dstAddress}, reconfigureParams.getSubSessionIdList());
+        verify(mUwbSessionNotificationManager).onControleeRemoved(eq(uwbSession));
+        verify(mUwbSessionNotificationManager).onRangingReconfigured(eq(uwbSession));
+    }
+
+    @Test
+    public void execReconfigure_nativeUpdateFailed() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        FiraRangingReconfigureParams reconfigureParams =
+                buildReconfigureParams();
+        when(mNativeUwbManager
+                .controllerMulticastListUpdate(anyInt(), anyInt(), anyInt(), any(), any()))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_FAILED);
+
+        mUwbSessionManager.reconfigure(uwbSession.getSessionHandle(), reconfigureParams);
+        mTestLooper.dispatchNext();
+
+        verify(mUwbSessionNotificationManager).onControleeAddFailed(eq(uwbSession),
+                eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mUwbSessionNotificationManager).onRangingReconfigureFailed(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+    }
+
+    @Test
+    public void execReconfigure_uwbSessionUpdateFailed() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        FiraRangingReconfigureParams reconfigureParams =
+                buildReconfigureParams();
+        when(mNativeUwbManager
+                .controllerMulticastListUpdate(anyInt(), anyInt(), anyInt(), any(), any()))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+        UwbMulticastListUpdateStatus uwbMulticastListUpdateStatus =
+                mock(UwbMulticastListUpdateStatus.class);
+        when(uwbMulticastListUpdateStatus.getNumOfControlee()).thenReturn(1);
+        when(uwbMulticastListUpdateStatus.getStatus()).thenReturn(
+                new int[] { UwbUciConstants.STATUS_CODE_FAILED });
+        doReturn(uwbMulticastListUpdateStatus).when(uwbSession).getMulticastListUpdateStatus();
+
+        mUwbSessionManager.reconfigure(uwbSession.getSessionHandle(), reconfigureParams);
+        mTestLooper.dispatchNext();
+
+        verify(mUwbSessionNotificationManager).onControleeAddFailed(eq(uwbSession),
+                eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mUwbSessionNotificationManager).onRangingReconfigureFailed(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+    }
+
+    @Test
+    public void execReconfigure_setAppConfigurationsFailed() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        FiraRangingReconfigureParams reconfigureParams =
+                buildReconfigureParams();
+        when(mNativeUwbManager
+                .controllerMulticastListUpdate(anyInt(), anyInt(), anyInt(), any(), any()))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_FAILED);
+        UwbMulticastListUpdateStatus uwbMulticastListUpdateStatus =
+                mock(UwbMulticastListUpdateStatus.class);
+        when(uwbMulticastListUpdateStatus.getStatus()).thenReturn(
+                new int[] { UwbUciConstants.STATUS_CODE_OK });
+        doReturn(uwbMulticastListUpdateStatus).when(uwbSession).getMulticastListUpdateStatus();
+        when(mUwbConfigurationManager.setAppConfigurations(anyInt(), any()))
+                .thenReturn(UwbUciConstants.STATUS_CODE_FAILED);
+
+        mUwbSessionManager.reconfigure(uwbSession.getSessionHandle(), reconfigureParams);
+        mTestLooper.dispatchNext();
+
+        verify(mUwbSessionNotificationManager).onRangingReconfigureFailed(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+    }
+
+    @Test
+    public void deInitSession() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+
+        mUwbSessionManager.deInitSession(uwbSession.getSessionHandle());
+
+        assertThat(mTestLooper.nextMessage().what).isEqualTo(5); // SESSION_CLOSE
+    }
+
+    @Test
+    public void execCloseSession_success() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        when(mNativeUwbManager.deInitSession(TEST_SESSION_ID))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+
+        mUwbSessionManager.deInitSession(uwbSession.getSessionHandle());
+        mTestLooper.dispatchNext();
+
+        verify(mUwbSessionNotificationManager).onRangingClosed(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_OK));
+        verify(mUwbMetrics).logRangingCloseEvent(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_OK));
+        assertThat(mUwbSessionManager.getSessionCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void execCloseSession_failed() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        when(mNativeUwbManager.deInitSession(TEST_SESSION_ID))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_FAILED);
+
+        mUwbSessionManager.deInitSession(uwbSession.getSessionHandle());
+        mTestLooper.dispatchNext();
+
+        verify(mUwbSessionNotificationManager).onRangingClosed(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+        verify(mUwbMetrics).logRangingCloseEvent(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+        assertThat(mUwbSessionManager.getSessionCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void onSessionStatusNotification_session_deinit() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        when(mNativeUwbManager.deInitSession(TEST_SESSION_ID))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK);
+
+        mUwbSessionManager.onSessionStatusNotificationReceived(
+                uwbSession.getSessionId(), UwbUciConstants.UWB_SESSION_STATE_DEINIT,
+                UwbUciConstants.REASON_STATE_CHANGE_WITH_SESSION_MANAGEMENT_COMMANDS);
+        mTestLooper.dispatchNext();
+
+        verify(mUwbSessionNotificationManager).onRangingClosedWithApiReasonCode(
+                eq(uwbSession), eq(RangingChangeReason.SYSTEM_POLICY));
+        verify(mUwbMetrics).logRangingCloseEvent(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_OK));
+        assertThat(mUwbSessionManager.getSessionCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testHandleClientDeath() throws Exception {
+        UwbSession uwbSession = prepareExistingUwbSession();
+        when(mNativeUwbManager.deInitSession(TEST_SESSION_ID))
+                .thenReturn((byte) UwbUciConstants.STATUS_CODE_FAILED);
+
+        uwbSession.binderDied();
+
+        verify(mUwbMetrics).logRangingCloseEvent(
+                eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED));
+        assertThat(mUwbSessionManager.getSessionCount()).isEqualTo(0);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/UwbSessionNotificationManagerTest.java b/service/tests/src/com/android/server/uwb/UwbSessionNotificationManagerTest.java
new file mode 100644
index 0000000..b5f286f
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/UwbSessionNotificationManagerTest.java
@@ -0,0 +1,301 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.AttributionSource;
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Pair;
+import android.uwb.IUwbRangingCallbacks;
+import android.uwb.RangingChangeReason;
+import android.uwb.RangingReport;
+import android.uwb.SessionHandle;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.data.UwbRangingData;
+import com.android.server.uwb.data.UwbUciConstants;
+
+import com.google.uwb.support.fira.FiraOpenSessionParams;
+import com.google.uwb.support.fira.FiraParams;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link com.android.server.uwb.UwbSettingsStore}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class UwbSessionNotificationManagerTest {
+    private static final long TEST_ELAPSED_NANOS = 100L;
+    private static final int UID = 343453;
+    private static final String PACKAGE_NAME = "com.uwb.test";
+    private static final AttributionSource ATTRIBUTION_SOURCE =
+            new AttributionSource.Builder(UID).setPackageName(PACKAGE_NAME).build();
+
+    @Mock private UwbInjector mUwbInjector;
+    @Mock private UwbSessionManager.UwbSession mUwbSession;
+    @Mock private SessionHandle mSessionHandle;
+    @Mock private IUwbRangingCallbacks mIUwbRangingCallbacks;
+    @Mock private FiraOpenSessionParams mFiraParams;
+
+    private UwbSessionNotificationManager mUwbSessionNotificationManager;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        when(mUwbSession.getSessionHandle()).thenReturn(mSessionHandle);
+        when(mUwbSession.getIUwbRangingCallbacks()).thenReturn(mIUwbRangingCallbacks);
+        when(mUwbSession.getProtocolName()).thenReturn(FiraParams.PROTOCOL_NAME);
+        when(mUwbSession.getParams()).thenReturn(mFiraParams);
+        when(mUwbSession.getAttributionSource()).thenReturn(ATTRIBUTION_SOURCE);
+        when(mFiraParams.getAoaResultRequest()).thenReturn(
+                FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS);
+        when(mFiraParams.hasResultReportPhase()).thenReturn(false);
+        when(mUwbInjector.checkUwbRangingPermissionForDataDelivery(any(), any())).thenReturn(true);
+        when(mUwbInjector.getElapsedSinceBootNanos()).thenReturn(TEST_ELAPSED_NANOS);
+        mUwbSessionNotificationManager = new UwbSessionNotificationManager(mUwbInjector);
+    }
+
+    /**
+     * Called after each testGG
+     */
+    @After
+    public void cleanup() {
+        validateMockitoUsage();
+    }
+
+    @Test
+    public void testOnRangingResultWithoutUwbRangingPermission() throws Exception {
+        Pair<UwbRangingData, RangingReport> testRangingDataAndRangingReport =
+                UwbTestUtils.generateRangingDataAndRangingReport(
+                        true, true, false, false, TEST_ELAPSED_NANOS);
+        when(mUwbInjector.checkUwbRangingPermissionForDataDelivery(eq(ATTRIBUTION_SOURCE), any()))
+                .thenReturn(false);
+        mUwbSessionNotificationManager.onRangingResult(
+                mUwbSession, testRangingDataAndRangingReport.first);
+
+        verify(mIUwbRangingCallbacks, never()).onRangingResult(any(), any());
+    }
+
+    @Test
+    public void testOnRangingResultWithAoa() throws Exception {
+        Pair<UwbRangingData, RangingReport> testRangingDataAndRangingReport =
+                UwbTestUtils.generateRangingDataAndRangingReport(
+                        true, true, false, false, TEST_ELAPSED_NANOS);
+        mUwbSessionNotificationManager.onRangingResult(
+                mUwbSession, testRangingDataAndRangingReport.first);
+        verify(mIUwbRangingCallbacks).onRangingResult(
+                mSessionHandle, testRangingDataAndRangingReport.second);
+    }
+
+    @Test
+    public void testOnRangingResultWithNoAoa() throws Exception {
+        when(mFiraParams.getAoaResultRequest()).thenReturn(
+                FiraParams.AOA_RESULT_REQUEST_MODE_NO_AOA_REPORT);
+        Pair<UwbRangingData, RangingReport> testRangingDataAndRangingReport =
+                UwbTestUtils.generateRangingDataAndRangingReport(
+                        false, false, false, false, TEST_ELAPSED_NANOS);
+        mUwbSessionNotificationManager.onRangingResult(
+                mUwbSession, testRangingDataAndRangingReport.first);
+        verify(mIUwbRangingCallbacks).onRangingResult(
+                mSessionHandle, testRangingDataAndRangingReport.second);
+    }
+
+    @Test
+    public void testOnRangingResultWithNoAoaElevation() throws Exception {
+        when(mFiraParams.getAoaResultRequest()).thenReturn(
+                FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_AZIMUTH_ONLY);
+        Pair<UwbRangingData, RangingReport> testRangingDataAndRangingReport =
+                UwbTestUtils.generateRangingDataAndRangingReport(
+                        true, false, false, false, TEST_ELAPSED_NANOS);
+        mUwbSessionNotificationManager.onRangingResult(
+                mUwbSession, testRangingDataAndRangingReport.first);
+        verify(mIUwbRangingCallbacks).onRangingResult(
+                mSessionHandle, testRangingDataAndRangingReport.second);
+    }
+
+    @Test
+    public void testOnRangingResultWithNoAoaAzimuth() throws Exception {
+        when(mFiraParams.getAoaResultRequest()).thenReturn(
+                FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS_ELEVATION_ONLY);
+        Pair<UwbRangingData, RangingReport> testRangingDataAndRangingReport =
+                UwbTestUtils.generateRangingDataAndRangingReport(
+                        false, true, false, false, TEST_ELAPSED_NANOS);
+        mUwbSessionNotificationManager.onRangingResult(
+                mUwbSession, testRangingDataAndRangingReport.first);
+        verify(mIUwbRangingCallbacks).onRangingResult(
+                mSessionHandle, testRangingDataAndRangingReport.second);
+    }
+  
+    @Test
+    public void testOnRangingResultWithAoaAndDestAoa() throws Exception {
+        when(mFiraParams.getAoaResultRequest()).thenReturn(
+                FiraParams.AOA_RESULT_REQUEST_MODE_REQ_AOA_RESULTS);
+        when(mFiraParams.hasResultReportPhase()).thenReturn(true);
+        when(mFiraParams.hasAngleOfArrivalAzimuthReport()).thenReturn(true);
+        when(mFiraParams.hasAngleOfArrivalElevationReport()).thenReturn(true);
+        Pair<UwbRangingData, RangingReport> testRangingDataAndRangingReport =
+                UwbTestUtils.generateRangingDataAndRangingReport(
+                        true, true, true, true, TEST_ELAPSED_NANOS);
+        mUwbSessionNotificationManager.onRangingResult(
+                mUwbSession, testRangingDataAndRangingReport.first);
+        verify(mIUwbRangingCallbacks).onRangingResult(
+                mSessionHandle, testRangingDataAndRangingReport.second);
+    }
+
+
+    @Test
+    public void testOnRangingOpened() throws Exception {
+        mUwbSessionNotificationManager.onRangingOpened(mUwbSession);
+
+        verify(mIUwbRangingCallbacks).onRangingOpened(mSessionHandle);
+    }
+
+    @Test
+    public void testOnRangingOpenFailed() throws Exception {
+        int status = UwbUciConstants.STATUS_CODE_ERROR_MAX_SESSIONS_EXCEEDED;
+        mUwbSessionNotificationManager.onRangingOpenFailed(mUwbSession, status);
+
+        verify(mIUwbRangingCallbacks).onRangingOpenFailed(eq(mSessionHandle),
+                eq(UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(status)),
+                argThat(p -> (p.getInt("status_code")) == status));
+    }
+
+    @Test
+    public void testOnRangingStarted() throws Exception {
+        mUwbSessionNotificationManager.onRangingStarted(mUwbSession, mUwbSession.getParams());
+
+        verify(mIUwbRangingCallbacks).onRangingStarted(mSessionHandle,
+                mUwbSession.getParams().toBundle());
+    }
+
+    @Test
+    public void testOnRangingStartFailed() throws Exception {
+        int status =  UwbUciConstants.STATUS_CODE_INVALID_PARAM;
+        mUwbSessionNotificationManager.onRangingStartFailed(mUwbSession, status);
+
+        verify(mIUwbRangingCallbacks).onRangingStartFailed(eq(mSessionHandle),
+                eq(UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(status)),
+                argThat(p -> (p.getInt("status_code")) == status));
+    }
+
+    @Test
+    public void testOnRangingStopped() throws Exception {
+        int status = UwbUciConstants.REASON_STATE_CHANGE_WITH_SESSION_MANAGEMENT_COMMANDS;
+        mUwbSessionNotificationManager.onRangingStopped(mUwbSession, status);
+
+        verify(mIUwbRangingCallbacks).onRangingStopped(eq(mSessionHandle),
+                eq(UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(status)),
+                argThat(p-> p.getInt("status_code") == status));
+    }
+
+    @Test
+    public void testORangingStopFailed() throws Exception {
+        int status = UwbUciConstants.STATUS_CODE_INVALID_RANGE;
+        mUwbSessionNotificationManager.onRangingStopFailed(mUwbSession, status);
+
+        verify(mIUwbRangingCallbacks).onRangingStopFailed(eq(mSessionHandle),
+                eq(UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(status)),
+                argThat(p -> (p.getInt("status_code")) == status));
+    }
+
+    @Test
+    public void testOnRangingReconfigured() throws Exception {
+        mUwbSessionNotificationManager.onRangingReconfigured(mUwbSession);
+
+        verify(mIUwbRangingCallbacks).onRangingReconfigured(eq(mSessionHandle), any());
+    }
+
+    @Test
+    public void testOnRangingReconfigureFailed() throws Exception {
+        int status =  UwbUciConstants.STATUS_CODE_INVALID_MESSAGE_SIZE;
+        mUwbSessionNotificationManager.onRangingReconfigureFailed(mUwbSession, status);
+
+        verify(mIUwbRangingCallbacks).onRangingReconfigureFailed(eq(mSessionHandle),
+                eq(UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(status)),
+                argThat(p -> (p.getInt("status_code")) == status));
+    }
+
+    @Test
+    public void testOnControleeAdded() throws Exception {
+        mUwbSessionNotificationManager.onControleeAdded(mUwbSession);
+
+        verify(mIUwbRangingCallbacks).onControleeAdded(eq(mSessionHandle), any());
+    }
+
+    @Test
+    public void testOnControleeAddFailed() throws Exception {
+        int status =  UwbUciConstants.STATUS_CODE_INVALID_MESSAGE_SIZE;
+        mUwbSessionNotificationManager.onControleeAddFailed(mUwbSession, status);
+
+        verify(mIUwbRangingCallbacks).onControleeAddFailed(eq(mSessionHandle),
+                eq(UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(status)),
+                argThat(p -> (p.getInt("status_code")) == status));
+    }
+
+    @Test
+    public void testOnControleeRemoved() throws Exception {
+        mUwbSessionNotificationManager.onControleeRemoved(mUwbSession);
+
+        verify(mIUwbRangingCallbacks).onControleeRemoved(eq(mSessionHandle), any());
+    }
+
+    @Test
+    public void testOnControleeRemoveFailed() throws Exception {
+        int status =  UwbUciConstants.STATUS_CODE_INVALID_MESSAGE_SIZE;
+        mUwbSessionNotificationManager.onControleeRemoveFailed(mUwbSession, status);
+
+        verify(mIUwbRangingCallbacks).onControleeRemoveFailed(eq(mSessionHandle),
+                eq(UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(status)),
+                argThat(p -> (p.getInt("status_code")) == status));
+    }
+
+    @Test
+    public void testOnRangingClosed() throws Exception {
+        int status = UwbUciConstants.REASON_ERROR_SLOT_LENGTH_NOT_SUPPORTED;
+        mUwbSessionNotificationManager.onRangingClosed(mUwbSession, status);
+
+        verify(mIUwbRangingCallbacks).onRangingClosed(eq(mSessionHandle),
+                eq(UwbSessionNotificationHelper.convertUciStatusToApiReasonCode(status)),
+                argThat(p-> p.getInt("status_code") == status));
+    }
+
+    @Test
+    public void testOnRangingClosedWithReasonCode() throws Exception {
+        int reasonCode = RangingChangeReason.SYSTEM_POLICY;
+        mUwbSessionNotificationManager.onRangingClosedWithApiReasonCode(mUwbSession, reasonCode);
+
+        verify(mIUwbRangingCallbacks).onRangingClosed(eq(mSessionHandle),
+                eq(reasonCode),
+                argThat(p-> p.isEmpty()));
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/UwbSettingsStoreTest.java b/service/tests/src/com/android/server/uwb/UwbSettingsStoreTest.java
new file mode 100644
index 0000000..903a1ce
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/UwbSettingsStoreTest.java
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb;
+
+import static com.android.server.uwb.UwbSettingsStore.SETTINGS_TOGGLE_STATE;
+import static com.android.server.uwb.UwbSettingsStore.SETTINGS_TOGGLE_STATE_KEY_FOR_MIGRATION;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.PersistableBundle;
+import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.AtomicFile;
+import android.uwb.UwbManager;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+
+
+/**
+ * Unit tests for {@link com.android.server.uwb.UwbSettingsStore}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class UwbSettingsStoreTest {
+    @Mock private Context mContext;
+    @Mock private AtomicFile mAtomicFile;
+    @Mock private UwbInjector mUwbInjector;
+
+    private TestLooper mLooper;
+    private UwbSettingsStore mUwbSettingsStore;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mLooper = new TestLooper();
+
+        FileOutputStream fos = mock(FileOutputStream.class);
+        when(mAtomicFile.startWrite()).thenReturn(fos);
+        mUwbSettingsStore = new UwbSettingsStore(
+                mContext, new Handler(mLooper.getLooper()), mAtomicFile, mUwbInjector);
+    }
+
+    /**
+     * Called after each test
+     */
+    @After
+    public void cleanup() {
+        validateMockitoUsage();
+    }
+
+    @Test
+    public void testSetterGetter() throws Exception {
+        assertThat(mUwbSettingsStore.get(SETTINGS_TOGGLE_STATE)).isTrue();
+        mUwbSettingsStore.put(SETTINGS_TOGGLE_STATE, false);
+        mLooper.dispatchAll();
+        assertThat(mUwbSettingsStore.get(SETTINGS_TOGGLE_STATE)).isFalse();
+
+        // Confirm that file writes have been triggered.
+        verify(mAtomicFile).startWrite();
+        verify(mAtomicFile).finishWrite(any());
+    }
+
+    @Test
+    public void testChangeListener() {
+        UwbSettingsStore.OnSettingsChangedListener listener = mock(
+                UwbSettingsStore.OnSettingsChangedListener.class);
+        mUwbSettingsStore.registerChangeListener(SETTINGS_TOGGLE_STATE, listener,
+                new Handler(mLooper.getLooper()));
+
+        mUwbSettingsStore.put(SETTINGS_TOGGLE_STATE, true);
+        mLooper.dispatchAll();
+        verify(listener).onSettingsChanged(SETTINGS_TOGGLE_STATE, true);
+
+        mUwbSettingsStore.unregisterChangeListener(SETTINGS_TOGGLE_STATE, listener);
+        mUwbSettingsStore.put(SETTINGS_TOGGLE_STATE, false);
+        mLooper.dispatchAll();
+        verifyNoMoreInteractions(listener);
+    }
+
+    @Test
+    public void testLoadFromStore() throws Exception {
+        byte[] data = createXmlForParsing(SETTINGS_TOGGLE_STATE.key, false);
+        setupAtomicFileMockForRead(data);
+
+        // Trigger file read.
+        mUwbSettingsStore.initialize();
+        mLooper.dispatchAll();
+
+        // Return the persisted value.
+        assertThat(mUwbSettingsStore.get(SETTINGS_TOGGLE_STATE)).isFalse();
+
+        // No write should be triggered on load.
+        verify(mAtomicFile, never()).startWrite();
+    }
+
+    @Test
+    public void testMigrationWhenStoreFileEmptyOrNotFound() throws Exception {
+        doThrow(new FileNotFoundException()).when(mAtomicFile).openRead();
+
+        // Toggle off before migration.
+        when(mUwbInjector.getSettingsInt(SETTINGS_TOGGLE_STATE_KEY_FOR_MIGRATION)).thenReturn(
+                UwbManager.AdapterStateCallback.STATE_DISABLED);
+
+        // Trigger file read.
+        mUwbSettingsStore.initialize();
+        mLooper.dispatchAll();
+
+        // Return the migrated value.
+        assertThat(mUwbSettingsStore.get(SETTINGS_TOGGLE_STATE)).isFalse();
+
+        // Write should be triggered after migration.
+        verify(mAtomicFile, times(1)).startWrite();
+    }
+
+
+    @Test
+    public void testNoMigrationLoadFromStoreWhenStoreFileEmptyOrNotFound() throws Exception {
+        doThrow(new FileNotFoundException()).when(mAtomicFile).openRead();
+        doThrow(new Settings.SettingNotFoundException("")).when(mUwbInjector).getSettingsInt(
+                SETTINGS_TOGGLE_STATE_KEY_FOR_MIGRATION);
+
+        // Trigger file read.
+        mUwbSettingsStore.initialize();
+        mLooper.dispatchAll();
+
+        // Return the default value.
+        assertThat(mUwbSettingsStore.get(SETTINGS_TOGGLE_STATE)).isTrue();
+
+        // No write should be triggered on load since no migration was done.
+        verify(mAtomicFile, never()).startWrite();
+    }
+
+    private byte[] createXmlForParsing(String key, Boolean value) throws Exception {
+        PersistableBundle bundle = new PersistableBundle();
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        bundle.putBoolean(key, value);
+        bundle.writeToStream(outputStream);
+        return outputStream.toByteArray();
+    }
+
+    private void setupAtomicFileMockForRead(byte[] dataToRead) throws Exception {
+        FileInputStream is = mock(FileInputStream.class);
+        when(mAtomicFile.openRead()).thenReturn(is);
+        when(is.available())
+                .thenReturn(dataToRead.length)
+                .thenReturn(0);
+        doAnswer(invocation -> {
+            byte[] data = invocation.getArgument(0);
+            int pos = invocation.getArgument(1);
+            if (pos == dataToRead.length) return 0; // read complete.
+            System.arraycopy(dataToRead, 0, data, 0, dataToRead.length);
+            return dataToRead.length;
+        }).when(is).read(any(), anyInt(), anyInt());
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/UwbShellCommandTest.java b/service/tests/src/com/android/server/uwb/UwbShellCommandTest.java
new file mode 100644
index 0000000..3d5504e
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/UwbShellCommandTest.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.uwb;
+
+import static android.uwb.RangingSession.Callback.REASON_LOCAL_REQUEST;
+
+import static com.android.server.uwb.UwbShellCommand.DEFAULT_CCC_OPEN_RANGING_PARAMS;
+import static com.android.server.uwb.UwbShellCommand.DEFAULT_FIRA_OPEN_SESSION_PARAMS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Binder;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.util.Pair;
+import android.uwb.IUwbRangingCallbacks;
+import android.uwb.RangingMeasurement;
+import android.uwb.RangingReport;
+import android.uwb.SessionHandle;
+import android.uwb.UwbManager;
+import android.uwb.UwbTestUtils;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.uwb.support.base.Params;
+import com.google.uwb.support.ccc.CccOpenRangingParams;
+import com.google.uwb.support.ccc.CccStartRangingParams;
+import com.google.uwb.support.fira.FiraOpenSessionParams;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+
+/**
+ * Unit tests for {@link com.android.server.uwb.UwbShellCommand}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UwbShellCommandTest {
+    private static final String TEST_PACKAGE = "com.android.test";
+
+    @Mock UwbInjector mUwbInjector;
+    @Mock UwbServiceImpl mUwbService;
+    @Mock UwbCountryCode mUwbCountryCode;
+    @Mock Context mContext;
+
+    UwbShellCommand mUwbShellCommand;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mUwbInjector.getUwbCountryCode()).thenReturn(mUwbCountryCode);
+
+        mUwbShellCommand = new UwbShellCommand(mUwbInjector, mUwbService, mContext);
+
+        // by default emulate shell uid.
+        BinderUtil.setUid(Process.SHELL_UID);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mUwbShellCommand.reset();
+        validateMockitoUsage();
+    }
+
+    @Test
+    public void testStatus() throws Exception {
+        when(mUwbService.getAdapterState())
+                .thenReturn(UwbManager.AdapterStateCallback.STATE_ENABLED_ACTIVE);
+
+        // unrooted shell.
+        mUwbShellCommand.exec(
+                new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(),
+                new String[]{"status"});
+        verify(mUwbService).getAdapterState();
+    }
+
+    @Test
+    public void testForceSetCountryCode() throws Exception {
+        // not allowed for unrooted shell.
+        mUwbShellCommand.exec(
+                new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(),
+                new String[]{"force-country-code", "enabled", "US"});
+        verify(mUwbCountryCode, never()).setOverrideCountryCode(any());
+        assertThat(mUwbShellCommand.getErrPrintWriter().toString().isEmpty()).isFalse();
+
+        BinderUtil.setUid(Process.ROOT_UID);
+
+        // rooted shell.
+        mUwbShellCommand.exec(
+                new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(),
+                new String[]{"force-country-code", "enabled", "US"});
+        verify(mUwbCountryCode).setOverrideCountryCode(any());
+
+    }
+
+    @Test
+    public void testForceClearCountryCode() throws Exception {
+        // not allowed for unrooted shell.
+        mUwbShellCommand.exec(
+                new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(),
+                new String[]{"force-country-code", "disabled"});
+        verify(mUwbCountryCode, never()).setOverrideCountryCode(any());
+        assertThat(mUwbShellCommand.getErrPrintWriter().toString().isEmpty()).isFalse();
+
+        BinderUtil.setUid(Process.ROOT_UID);
+
+        // rooted shell.
+        mUwbShellCommand.exec(
+                new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(),
+                new String[]{"force-country-code", "disabled"});
+        verify(mUwbCountryCode).clearOverrideCountryCode();
+    }
+
+    @Test
+    public void testGetCountryCode() throws Exception {
+        mUwbShellCommand.exec(
+                new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(),
+                new String[]{"get-country-code"});
+        verify(mUwbCountryCode).getCountryCode();
+    }
+
+    @Test
+    public void testEnableUwb() throws Exception {
+        mUwbShellCommand.exec(
+                new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(),
+                new String[]{"enable-uwb"});
+        verify(mUwbService).setEnabled(true);
+    }
+
+    @Test
+    public void testDisableUwb() throws Exception {
+        mUwbShellCommand.exec(
+                new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(),
+                new String[]{"disable-uwb"});
+        verify(mUwbService).setEnabled(false);
+    }
+
+    private static class MutableCb {
+        @Nullable public IUwbRangingCallbacks cb;
+    }
+
+    private Pair<IUwbRangingCallbacks, SessionHandle> triggerAndVerifyRangingStart(
+            String[] rangingStartCmd, @NonNull Params openRangingParams) throws Exception {
+        return triggerAndVerifyRangingStart(rangingStartCmd, openRangingParams, null);
+    }
+
+    private Pair<IUwbRangingCallbacks, SessionHandle> triggerAndVerifyRangingStart(
+            String[] rangingStartCmd, @NonNull Params openRangingParams, @Nullable Params
+            startRangingParams) throws Exception {
+        final MutableCb cbCaptor = new MutableCb();
+        doAnswer(invocation -> {
+            cbCaptor.cb = invocation.getArgument(2);
+            cbCaptor.cb.onRangingOpened(invocation.getArgument(1));
+            return true;
+        }).when(mUwbService).openRanging(any(), any(), any(), any(), any());
+        doAnswer(invocation -> {
+            cbCaptor.cb.onRangingStarted(invocation.getArgument(0), new PersistableBundle());
+            return true;
+        }).when(mUwbService).startRanging(any(), any());
+
+        mUwbShellCommand.exec(
+                new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(),
+                rangingStartCmd);
+
+        ArgumentCaptor<SessionHandle> sessionHandleCaptor =
+                ArgumentCaptor.forClass(SessionHandle.class);
+        ArgumentCaptor<PersistableBundle> paramsCaptor =
+                ArgumentCaptor.forClass(PersistableBundle.class);
+
+        verify(mUwbService).openRanging(
+                eq(new AttributionSource.Builder(Process.SHELL_UID)
+                        .setPackageName(UwbShellCommand.SHELL_PACKAGE_NAME)
+                        .build()),
+                sessionHandleCaptor.capture(), any(), paramsCaptor.capture(), any());
+        // PersistableBundle does not implement equals, so use toString equals.
+        assertThat(paramsCaptor.getValue().toString())
+                .isEqualTo(openRangingParams.toBundle().toString());
+
+        verify(mUwbService).startRanging(
+                eq(sessionHandleCaptor.getValue()), paramsCaptor.capture());
+        assertThat(paramsCaptor.getValue().toString())
+                .isEqualTo(startRangingParams != null
+                        ? startRangingParams.toBundle().toString()
+                        : new PersistableBundle().toString());
+
+        return Pair.create(cbCaptor.cb, sessionHandleCaptor.getValue());
+    }
+
+    private void triggerAndVerifyRangingStop(
+            String[] rangingStopCmd, IUwbRangingCallbacks cb, SessionHandle sessionHandle)
+            throws Exception {
+        doAnswer(invocation -> {
+            cb.onRangingStopped(sessionHandle, REASON_LOCAL_REQUEST, new PersistableBundle());
+            return true;
+        }).when(mUwbService).stopRanging(any());
+        doAnswer(invocation -> {
+            cb.onRangingClosed(
+                    sessionHandle, REASON_LOCAL_REQUEST,
+                    new PersistableBundle());
+            return true;
+        }).when(mUwbService).closeRanging(any());
+
+        mUwbShellCommand.exec(
+                new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(),
+                rangingStopCmd);
+
+        verify(mUwbService).stopRanging(sessionHandle);
+        verify(mUwbService).closeRanging(sessionHandle);
+    }
+
+    private CccStartRangingParams getCccStartRangingParamsFromOpenRangingParams(
+            @NonNull CccOpenRangingParams openRangingParams) {
+        return new CccStartRangingParams.Builder()
+                .setSessionId(openRangingParams.getSessionId())
+                .setRanMultiplier(openRangingParams.getRanMultiplier())
+                .build();
+    }
+
+    @Test
+    public void testStartFiraRanging() throws Exception {
+        triggerAndVerifyRangingStart(
+                new String[]{"start-fira-ranging-session"},
+                DEFAULT_FIRA_OPEN_SESSION_PARAMS.build());
+    }
+
+    @Test
+    public void testStartFiraRangingWithNonDefaultParams() throws Exception {
+        FiraOpenSessionParams.Builder openSessionParamsBuilder =
+                new FiraOpenSessionParams.Builder(DEFAULT_FIRA_OPEN_SESSION_PARAMS);
+        openSessionParamsBuilder.setSessionId(5);
+        triggerAndVerifyRangingStart(
+                new String[]{"start-fira-ranging-session", "-i", "5"},
+                openSessionParamsBuilder.build());
+    }
+
+    @Test
+    public void testStartFiraRangingWithBothInterleavingAndAoaResultReq() throws Exception {
+        // Both AOA result req and interleaving are not allowed in the same command.
+        assertThat(mUwbShellCommand.exec(
+                new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(),
+                new String[]{"start-fira-ranging-session", "-i", "5", "-z", "4,5,6", "-e",
+                        "enabled"})).isEqualTo(-1);
+    }
+
+    private RangingMeasurement getRangingMeasurement() {
+        return new RangingMeasurement.Builder()
+                .setStatus(RangingMeasurement.RANGING_STATUS_SUCCESS)
+                .setElapsedRealtimeNanos(67)
+                .setDistanceMeasurement(UwbTestUtils.getDistanceMeasurement())
+                .setAngleOfArrivalMeasurement(UwbTestUtils.getAngleOfArrivalMeasurement())
+                .setRemoteDeviceAddress(UwbTestUtils.getUwbAddress(true))
+                .build();
+    }
+
+    @Test
+    public void testRangingReportFiraRanging() throws Exception {
+        Pair<IUwbRangingCallbacks, SessionHandle> cbAndSessionHandle =
+                triggerAndVerifyRangingStart(
+                        new String[]{"start-fira-ranging-session"},
+                        DEFAULT_FIRA_OPEN_SESSION_PARAMS.build());
+        int sessionId = DEFAULT_FIRA_OPEN_SESSION_PARAMS.build().getSessionId();
+        cbAndSessionHandle.first.onRangingResult(
+                cbAndSessionHandle.second,
+                new RangingReport.Builder().addMeasurement(getRangingMeasurement()).build());
+        mUwbShellCommand.exec(
+                new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(),
+                new String[]{"get-ranging-session-reports", String.valueOf(sessionId)});
+    }
+
+    @Test
+    public void testRangingReportAllFiraRanging() throws Exception {
+        Pair<IUwbRangingCallbacks, SessionHandle> cbAndSessionHandle =
+                triggerAndVerifyRangingStart(
+                        new String[]{"start-fira-ranging-session"},
+                        DEFAULT_FIRA_OPEN_SESSION_PARAMS.build());
+        cbAndSessionHandle.first.onRangingResult(
+                cbAndSessionHandle.second,
+                new RangingReport.Builder().addMeasurement(getRangingMeasurement()).build());
+        mUwbShellCommand.exec(
+                new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(),
+                new String[]{"get-all-ranging-session-reports"});
+    }
+
+    @Test
+    public void testStopFiraRanging() throws Exception {
+        Pair<IUwbRangingCallbacks, SessionHandle> cbAndSessionHandle =
+                triggerAndVerifyRangingStart(
+                        new String[]{"start-fira-ranging-session"},
+                        DEFAULT_FIRA_OPEN_SESSION_PARAMS.build());
+        int sessionId = DEFAULT_FIRA_OPEN_SESSION_PARAMS.build().getSessionId();
+        triggerAndVerifyRangingStop(
+                new String[]{"stop-ranging-session", String.valueOf(sessionId)},
+                cbAndSessionHandle.first, cbAndSessionHandle.second);
+    }
+
+    @Test
+    public void testStartCccRanging() throws Exception {
+        CccOpenRangingParams openSessionParams = DEFAULT_CCC_OPEN_RANGING_PARAMS.build();
+        triggerAndVerifyRangingStart(
+                new String[]{"start-ccc-ranging-session"},
+                openSessionParams,
+                getCccStartRangingParamsFromOpenRangingParams(openSessionParams));
+    }
+
+    @Test
+    public void testStartCccRangingWithNonDefaultParams() throws Exception {
+        CccOpenRangingParams.Builder openSessionParamsBuilder =
+                new CccOpenRangingParams.Builder(DEFAULT_CCC_OPEN_RANGING_PARAMS);
+        openSessionParamsBuilder.setSessionId(5);
+        CccOpenRangingParams openSessionParams = openSessionParamsBuilder.build();
+        triggerAndVerifyRangingStart(
+                new String[]{"start-ccc-ranging-session", "-i", "5"},
+                openSessionParams,
+                getCccStartRangingParamsFromOpenRangingParams(openSessionParams));
+    }
+
+    @Test
+    public void testStopCccRanging() throws Exception {
+        CccOpenRangingParams openSessionParams = DEFAULT_CCC_OPEN_RANGING_PARAMS.build();
+        Pair<IUwbRangingCallbacks, SessionHandle> cbAndSessionHandle =
+                triggerAndVerifyRangingStart(
+                        new String[]{"start-ccc-ranging-session"},
+                        openSessionParams,
+                        getCccStartRangingParamsFromOpenRangingParams(openSessionParams));
+        int sessionId = openSessionParams.getSessionId();
+        triggerAndVerifyRangingStop(
+                new String[]{"stop-ranging-session", String.valueOf(sessionId)},
+                cbAndSessionHandle.first, cbAndSessionHandle.second);
+    }
+
+    @Test
+    public void testStopAllRanging() throws Exception {
+        CccOpenRangingParams openSessionParams = DEFAULT_CCC_OPEN_RANGING_PARAMS.build();
+        Pair<IUwbRangingCallbacks, SessionHandle> cbAndSessionHandle =
+                triggerAndVerifyRangingStart(
+                        new String[]{"start-ccc-ranging-session"},
+                        openSessionParams,
+                        getCccStartRangingParamsFromOpenRangingParams(openSessionParams));
+        triggerAndVerifyRangingStop(
+                new String[]{"stop-all-ranging-sessions"},
+                cbAndSessionHandle.first, cbAndSessionHandle.second);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/data/UwbMulticastListUpdateStatusTest.java b/service/tests/src/com/android/server/uwb/data/UwbMulticastListUpdateStatusTest.java
new file mode 100644
index 0000000..0f4bdbd
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/data/UwbMulticastListUpdateStatusTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.data;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+/**
+ * Unit tests for {@link com.android.server.uwb.data.UwbMulticastListUpdateStatus}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class UwbMulticastListUpdateStatusTest {
+    private static final long TEST_SESSION_ID = 1;
+    private static final int TEST_REMAINING_SIZE = 2;
+    private static final int TEST_NUM_OF_CONTROLLEES = 1;
+    private static final int[] TEST_CONTROLEE_ADDRESS = new int[] {0x0A, 0x04};
+    private static final long[] TEST_SUB_SESSION_ID = new long[] {1, 1};
+    private static final int[] TEST_STATUS = new int[] {0};
+
+    private UwbMulticastListUpdateStatus mUwbMulticastListUpdateStatus;
+
+    @Test
+    public void testInitializeUwbMulticastListUpdateStatus() throws Exception {
+        mUwbMulticastListUpdateStatus = new UwbMulticastListUpdateStatus(TEST_SESSION_ID,
+                TEST_REMAINING_SIZE, TEST_NUM_OF_CONTROLLEES, TEST_CONTROLEE_ADDRESS,
+                TEST_SUB_SESSION_ID, TEST_STATUS);
+
+        assertThat(mUwbMulticastListUpdateStatus.getSessionId()).isEqualTo(TEST_SESSION_ID);
+        assertThat(mUwbMulticastListUpdateStatus.getRemainingSize()).isEqualTo(TEST_REMAINING_SIZE);
+        assertThat(mUwbMulticastListUpdateStatus.getNumOfControlee())
+                .isEqualTo(TEST_NUM_OF_CONTROLLEES);
+        assertThat(mUwbMulticastListUpdateStatus.getContolleeMacAddress())
+                .isEqualTo(TEST_CONTROLEE_ADDRESS);
+        assertThat(mUwbMulticastListUpdateStatus.getSubSessionId()).isEqualTo(TEST_SUB_SESSION_ID);
+        assertThat(mUwbMulticastListUpdateStatus.getStatus()).isEqualTo(TEST_STATUS);
+
+        final String testString = "UwbMulticastListUpdateEvent { "
+                + " SessionID =" + TEST_SESSION_ID
+                + ", RemainingSize =" + TEST_REMAINING_SIZE
+                + ", NumOfControlee =" + TEST_NUM_OF_CONTROLLEES
+                + ", MacAddress =" + Arrays.toString(TEST_CONTROLEE_ADDRESS)
+                + ", SubSessionId =" + Arrays.toString(TEST_SUB_SESSION_ID)
+                + ", Status =" + Arrays.toString(TEST_STATUS)
+                + '}';
+
+        assertThat(mUwbMulticastListUpdateStatus.toString()).isEqualTo(testString);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/data/UwbRangingDataTest.java b/service/tests/src/com/android/server/uwb/data/UwbRangingDataTest.java
new file mode 100644
index 0000000..11828fe
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/data/UwbRangingDataTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.data;
+
+import static com.android.server.uwb.data.UwbUciConstants.RANGING_MEASUREMENT_TYPE_TWO_WAY;
+import static com.android.server.uwb.util.UwbUtil.convertFloatToQFormat;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.uwb.support.fira.FiraParams;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+/**
+ * Unit tests for {@link com.android.server.uwb.data.UwbRangingData}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class UwbRangingDataTest {
+    private static final long TEST_SEQ_COUNTER = 5;
+    private static final long TEST_SESSION_ID = 7;
+    private static final int TEST_RCR_INDICATION = 7;
+    private static final long TEST_CURR_RANGING_INTERVAL = 100;
+    private static final int TEST_RANGING_MEASURES_TYPE = RANGING_MEASUREMENT_TYPE_TWO_WAY;
+    private static final int TEST_MAC_ADDRESS_MODE = 1;
+    private static final byte[] TEST_MAC_ADDRESS = {0x1, 0x3};
+    private static final int TEST_STATUS = FiraParams.STATUS_CODE_OK;
+    private static final int TEST_LOS = 0;
+    private static final int TEST_DISTANCE = 101;
+    private static final float TEST_AOA_AZIMUTH = 67;
+    private static final int TEST_AOA_AZIMUTH_FOM = 50;
+    private static final float TEST_AOA_ELEVATION = 37;
+    private static final int TEST_AOA_ELEVATION_FOM = 90;
+    private static final float TEST_AOA_DEST_AZIMUTH = 67;
+    private static final int TEST_AOA_DEST_AZIMUTH_FOM = 50;
+    private static final float TEST_AOA_DEST_ELEVATION = 37;
+    private static final int TEST_AOA_DEST_ELEVATION_FOM = 90;
+    private static final int TEST_SLOT_IDX = 10;
+
+    private UwbRangingData mUwbRangingData;
+
+    @Test
+    public void testInitializeUwbRangingData() throws Exception {
+        final int noOfRangingMeasures = 1;
+        final UwbTwoWayMeasurement[] uwbTwoWayMeasurements =
+                new UwbTwoWayMeasurement[noOfRangingMeasures];
+        uwbTwoWayMeasurements[0] = new UwbTwoWayMeasurement(TEST_MAC_ADDRESS, TEST_STATUS, TEST_LOS,
+                TEST_DISTANCE, convertFloatToQFormat(TEST_AOA_AZIMUTH, 9, 7),
+                TEST_AOA_AZIMUTH_FOM, convertFloatToQFormat(TEST_AOA_ELEVATION, 9, 7),
+                TEST_AOA_ELEVATION_FOM, convertFloatToQFormat(TEST_AOA_DEST_AZIMUTH, 9, 7),
+                TEST_AOA_DEST_AZIMUTH_FOM, convertFloatToQFormat(TEST_AOA_DEST_ELEVATION, 9, 7),
+                TEST_AOA_DEST_ELEVATION_FOM, TEST_SLOT_IDX);
+        mUwbRangingData = new UwbRangingData(TEST_SEQ_COUNTER, TEST_SESSION_ID,
+                TEST_RCR_INDICATION, TEST_CURR_RANGING_INTERVAL, TEST_RANGING_MEASURES_TYPE,
+                TEST_MAC_ADDRESS_MODE, noOfRangingMeasures, uwbTwoWayMeasurements);
+
+        assertThat(mUwbRangingData.getSequenceCounter()).isEqualTo(TEST_SEQ_COUNTER);
+        assertThat(mUwbRangingData.getSessionId()).isEqualTo(TEST_SESSION_ID);
+        assertThat(mUwbRangingData.getRcrIndication()).isEqualTo(TEST_RCR_INDICATION);
+        assertThat(mUwbRangingData.getCurrRangingInterval()).isEqualTo(TEST_CURR_RANGING_INTERVAL);
+        assertThat(mUwbRangingData.getRangingMeasuresType()).isEqualTo(TEST_RANGING_MEASURES_TYPE);
+        assertThat(mUwbRangingData.getMacAddressMode()).isEqualTo(TEST_MAC_ADDRESS_MODE);
+        assertThat(mUwbRangingData.getNoOfRangingMeasures()).isEqualTo(1);
+
+        final String testString = "UwbRangingData { "
+                + " SeqCounter = " + TEST_SEQ_COUNTER
+                + ", SessionId = " + TEST_SESSION_ID
+                + ", RcrIndication = " + TEST_RCR_INDICATION
+                + ", CurrRangingInterval = " + TEST_CURR_RANGING_INTERVAL
+                + ", RangingMeasuresType = " + TEST_RANGING_MEASURES_TYPE
+                + ", MacAddressMode = " + TEST_MAC_ADDRESS_MODE
+                + ", NoOfRangingMeasures = " + noOfRangingMeasures
+                + ", RangingTwoWayMeasures = " + Arrays.toString(uwbTwoWayMeasurements)
+                + '}';
+
+        assertThat(mUwbRangingData.toString()).isEqualTo(testString);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/data/UwbVendorUciResponseTest.java b/service/tests/src/com/android/server/uwb/data/UwbVendorUciResponseTest.java
new file mode 100644
index 0000000..a61469c
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/data/UwbVendorUciResponseTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.data;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import org.junit.Test;
+
+public class UwbVendorUciResponseTest {
+    @Test
+    public void equalValues() {
+        UwbVendorUciResponse response1 = new UwbVendorUciResponse(
+                /* status= */ (byte) 0x0,
+                /* gid= */ 1,
+                /* oid= */ 2,
+                /* payload=*/ DataTypeConversionUtil.hexStringToByteArray("0A0B0A"));
+        UwbVendorUciResponse response2 = new UwbVendorUciResponse(
+                /* status= */ (byte) 0x0,
+                /* gid= */ 1,
+                /* oid= */ 2,
+                /* payload=*/ DataTypeConversionUtil.hexStringToByteArray("0A0B0A"));
+
+        assertThat(response1).isEqualTo(response2);
+    }
+
+    @Test
+    public void sameInstance() {
+        UwbVendorUciResponse response1 = new UwbVendorUciResponse(
+                /* status= */ (byte) 0x0,
+                /* gid= */ 1,
+                /* oid= */ 2,
+                /* payload=*/ DataTypeConversionUtil.hexStringToByteArray("0A0B0A"));
+        UwbVendorUciResponse response2 = response1;
+
+        assertThat(response1).isEqualTo(response2);
+    }
+
+    @Test
+    public void notEqualStatus() {
+        UwbVendorUciResponse response1 = new UwbVendorUciResponse(
+                /* status= */ (byte) 0x0,
+                /* gid= */ 1,
+                /* oid= */ 2,
+                /* payload=*/ DataTypeConversionUtil.hexStringToByteArray("0A0B0A"));
+        UwbVendorUciResponse response2 = new UwbVendorUciResponse(
+                /* status= */ (byte) 0x1,
+                /* gid= */ 1,
+                /* oid= */ 2,
+                /* payload=*/ DataTypeConversionUtil.hexStringToByteArray("0A0B0A"));
+
+        assertThat(response1).isNotEqualTo(response2);
+    }
+
+    @Test
+    public void notEqualGid() {
+        UwbVendorUciResponse response1 = new UwbVendorUciResponse(
+                /* status= */ (byte) 0x0,
+                /* gid= */ 1,
+                /* oid= */ 2,
+                /* payload=*/ DataTypeConversionUtil.hexStringToByteArray("0A0B0A"));
+        UwbVendorUciResponse response2 = new UwbVendorUciResponse(
+                /* status= */ (byte) 0x0,
+                /* gid= */ 2,
+                /* oid= */ 2,
+                /* payload=*/ DataTypeConversionUtil.hexStringToByteArray("0A0B0A"));
+
+        assertThat(response1).isNotEqualTo(response2);
+    }
+
+    @Test
+    public void notEqualOid() {
+        UwbVendorUciResponse response1 = new UwbVendorUciResponse(
+                /* status= */ (byte) 0x0,
+                /* gid= */ 1,
+                /* oid= */ 2,
+                /* payload=*/ DataTypeConversionUtil.hexStringToByteArray("0A0B0A"));
+        UwbVendorUciResponse response2 = new UwbVendorUciResponse(
+                /* status= */ (byte) 0x0,
+                /* gid= */ 1,
+                /* oid= */ 1,
+                /* payload=*/ DataTypeConversionUtil.hexStringToByteArray("0A0B0A"));
+
+        assertThat(response1).isNotEqualTo(response2);
+    }
+
+    @Test
+    public void notEqualPayload() {
+        UwbVendorUciResponse response1 = new UwbVendorUciResponse(
+                /* status= */ (byte) 0x0,
+                /* gid= */ 1,
+                /* oid= */ 2,
+                /* payload=*/ DataTypeConversionUtil.hexStringToByteArray("0A0B0A"));
+        UwbVendorUciResponse response2 = new UwbVendorUciResponse(
+                /* status= */ (byte) 0x0,
+                /* gid= */ 1,
+                /* oid= */ 2,
+                /* payload=*/ DataTypeConversionUtil.hexStringToByteArray("0A0B0B"));
+
+        assertThat(response1).isNotEqualTo(response2);
+    }
+
+    @Test
+    public void differentClasses() {
+        UwbVendorUciResponse response1 = new UwbVendorUciResponse(
+                /* status= */ (byte) 0x0,
+                /* gid= */ 1,
+                /* oid= */ 2,
+                /* payload=*/ DataTypeConversionUtil.hexStringToByteArray("0A0B0A"));
+
+        assertThat(response1).isNotEqualTo(0);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/discovery/ble/DiscoveryAdvertisementTest.java b/service/tests/src/com/android/server/uwb/discovery/ble/DiscoveryAdvertisementTest.java
new file mode 100644
index 0000000..975c4b1
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/discovery/ble/DiscoveryAdvertisementTest.java
@@ -0,0 +1,251 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.discovery.ble;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.discovery.info.ChannelPowerInfo;
+import com.android.server.uwb.discovery.info.FiraProfileSupportInfo;
+import com.android.server.uwb.discovery.info.FiraProfileSupportInfo.FiraProfile;
+import com.android.server.uwb.discovery.info.RegulatoryInfo;
+import com.android.server.uwb.discovery.info.RegulatoryInfo.SourceOfInfo;
+import com.android.server.uwb.discovery.info.SecureComponentInfo;
+import com.android.server.uwb.discovery.info.SecureComponentInfo.SecureComponentProtocolType;
+import com.android.server.uwb.discovery.info.SecureComponentInfo.SecureComponentType;
+import com.android.server.uwb.discovery.info.UwbIndicationData;
+import com.android.server.uwb.discovery.info.VendorSpecificData;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+/**
+ * Unit test for {@link DiscoveryAdvertisement}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DiscoveryAdvertisementTest {
+
+    private static final byte[] BYTES =
+            new byte[] {
+                0x20, 0x16, (byte) 0xF4, (byte) 0xFF,
+                // UwbIndicationData
+                0x14, (byte) 0b11101001, (byte) 0x9C, (byte) 0xF1, 0x11,
+                // RegulatoryInfo
+                0x39, 0x41, 0x55, 0x53, 0x62, 0x1E, 0x3B, 0x7F, (byte) 0xD8, (byte) 0x9F,
+                // FiraProfileSupportInfo
+                0x41, 0x1,
+                // VendorSpecificData
+                0x25, 0x75, 0x00, 0x10, (byte) 0xFF, 0x00,
+                // VendorSpecificData
+                0x25, 0x75, 0x00, 0x10, (byte) 0xFF, 0x00,
+            };
+    private static final String SERVICE_UUID = "FFF4";
+    private static final byte[] UWB_INDICATION_DATA_BYTES =
+            new byte[] {(byte) 0b11101001, (byte) 0x9C, (byte) 0xF1, 0x11};
+    private static final byte[] VENDOR_SPECIFIC_DATA_BYTES =
+            new byte[] {0x75, 0x00, 0x10, (byte) 0xFF, 0x00};
+    private static final byte[] REGULATORY_INFO_BYTES =
+            new byte[] {0x41, 0x55, 0x53, 0x62, 0x1E, 0x3B, 0x7F, (byte) 0xD8, (byte) 0x9F};
+    private static final byte[] FIRA_PROFILE_SUPPORT_INFO_BYTES = new byte[] {0x1};
+    private static final byte[] MIN_BYTES = new byte[] {0x03, 0x16, (byte) 0xF3, (byte) 0xFF};
+    private static final byte[] BYTES_NO_VENDOR =
+            new byte[] {
+                0x14, 0x16, (byte) 0xF4, (byte) 0xFF,
+                // UwbIndicationData
+                0x14, (byte) 0b11101001, (byte) 0x9C, (byte) 0xF1, 0x11,
+                // RegulatoryInfo
+                0x39, 0x41, 0x55, 0x53, 0x62, 0x1E, 0x3B, 0x7F, (byte) 0xD8, (byte) 0x9F,
+                // FiraProfileSupportInfo
+                0x41, 0x1
+            };
+
+    private static final DiscoveryAdvertisement ADVERTISEMENT =
+            new DiscoveryAdvertisement(
+                    DiscoveryAdvertisement.FIRA_CS_SERVICE_UUID,
+                    new UwbIndicationData(
+                            /*firaUwbSupport=*/ true,
+                            /*iso14443Support=*/ true,
+                            /*uwbRegulartoryInfoAvailableInAd=*/ true,
+                            /*uwbRegulartoryInfoAvailableInOob=*/ false,
+                            /*firaProfileInfoAvailableInAd=*/ true,
+                            /*firaProfileInfoAvailableInOob=*/ false,
+                            /*dualGapRoleSupport=*/ true,
+                            /*bluetoothRssiThresholdDbm=*/ -100,
+                            new SecureComponentInfo[] {
+                                new SecureComponentInfo(
+                                        /*staticIndication=*/ true,
+                                        /*secid=*/ 113,
+                                        SecureComponentType.ESE_NONREMOVABLE,
+                                        SecureComponentProtocolType
+                                                .FIRA_OOB_ADMINISTRATIVE_PROTOCOL)
+                            }),
+                    new RegulatoryInfo(
+                            SourceOfInfo.SATELLITE_NAVIGATION_SYSTEM,
+                            /*outdoorsTransmittionPermitted=*/ true,
+                            /*countryCode=*/ "US",
+                            /*timestampSecondsSinceEpoch=*/ 1646148479,
+                            new ChannelPowerInfo[] {
+                                new ChannelPowerInfo(
+                                        /*firstChannel=*/ 13,
+                                        /*numOfChannels=*/ 4,
+                                        /*isIndoor=*/ false,
+                                        /*averagePowerLimitDbm=*/ -97)
+                            }),
+                    new FiraProfileSupportInfo(new FiraProfile[] {FiraProfile.PACS}),
+                    new VendorSpecificData[] {
+                        new VendorSpecificData(
+                                /*firstChannel=*/ 117, new byte[] {0x10, (byte) 0xFF, 0x00}),
+                        new VendorSpecificData(
+                                /*firstChannel=*/ 117, new byte[] {0x10, (byte) 0xFF, 0x00}),
+                    });
+
+    @Test
+    public void fromBytes_emptyData() {
+        assertThat(DiscoveryAdvertisement.fromBytes(new byte[] {}, null)).isNull();
+    }
+
+    @Test
+    public void fromBytes_dataTooShort() {
+        assertThat(DiscoveryAdvertisement.fromBytes(new byte[] {0x0, 0x1, 0x2}, null)).isNull();
+    }
+
+    @Test
+    public void fromBytes_unmatedDataSize() {
+        // Specified data size is 0xF1, actual size is 8.
+        byte[] bytes = new byte[] {(byte) 0xF1, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01};
+        assertThat(DiscoveryAdvertisement.fromBytes(bytes, null)).isNull();
+    }
+
+    @Test
+    public void fromBytes_invalidDataType() {
+        // Specified data type is 0x55, expect 0x16.
+        byte[] bytes = new byte[] {0x08, 0x55, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01};
+        assertThat(DiscoveryAdvertisement.fromBytes(bytes, null)).isNull();
+    }
+
+    @Test
+    public void fromBytes_invalidServiceUuid() {
+        // Specified service uuid is 0xFFFF, expect 0xFFF3 or 0xFFF4.
+        byte[] bytes =
+                new byte[] {0x08, 0x16, (byte) 0xFF, (byte) 0xFF, 0x01, 0x01, 0x01, 0x01, 0x01};
+        assertThat(DiscoveryAdvertisement.fromBytes(bytes, null)).isNull();
+    }
+
+    @Test
+    public void fromBytes_dataEndedUnexpectedly() {
+        // Specified field size is 0xF, actual size is 4.
+        byte[] bytes =
+                new byte[] {0x08, 0x16, (byte) 0xF3, (byte) 0xFF, 0x1F, 0x01, 0x01, 0x01, 0x01};
+        assertThat(DiscoveryAdvertisement.fromBytes(bytes, null)).isNull();
+    }
+
+    @Test
+    public void fromBytes_invalidFieldType() {
+        // Specified field type is 0x9, expect 1-4
+        byte[] bytes =
+                new byte[] {
+                    0x08, 0x16, (byte) 0xF3, (byte) 0xFF, (byte) 0x94, 0x01, 0x01, 0x01, 0x01
+                };
+        assertThat(DiscoveryAdvertisement.fromBytes(bytes, null)).isNull();
+    }
+
+    @Test
+    public void fromBytes_vendorSpecificDataExistedInBothAd() {
+        // Specified service uuid is 0xFFFF, expect 0xFFF3 or 0xFFF4.
+        byte[] bytes =
+                new byte[] {0x08, 0x16, (byte) 0xF3, (byte) 0xFF, 0x24, 0x75, 0x00, 0x10, 0x20};
+        byte[] vendor_bytes = new byte[] {0x23, 0x75, 0x00, 0x10};
+        assertThat(DiscoveryAdvertisement.fromBytes(bytes, vendor_bytes)).isNull();
+    }
+
+    @Test
+    public void fromBytes_vendorSpecificDataEndedUnexpectedly() {
+        // Specified vendor data size is 0x4, actual size is 3.
+        byte[] vendor_bytes = new byte[] {0x24, 0x75, 0x00, 0x10};
+        assertThat(DiscoveryAdvertisement.fromBytes(MIN_BYTES, vendor_bytes)).isNull();
+    }
+
+    @Test
+    public void fromBytes_succeed() {
+        DiscoveryAdvertisement adv = DiscoveryAdvertisement.fromBytes(BYTES, null);
+        assertThat(adv).isNotNull();
+
+        assertThat(adv.serviceUuid).isEqualTo(SERVICE_UUID);
+        assertThat(UwbIndicationData.toBytes(adv.uwbIndicationData))
+                .isEqualTo(UWB_INDICATION_DATA_BYTES);
+        assertThat(RegulatoryInfo.toBytes(adv.regulatoryInfo)).isEqualTo(REGULATORY_INFO_BYTES);
+        assertThat(FiraProfileSupportInfo.toBytes(adv.firaProfileSupportInfo))
+                .isEqualTo(FIRA_PROFILE_SUPPORT_INFO_BYTES);
+        assertThat(adv.vendorSpecificData.length).isEqualTo(2);
+        assertThat(VendorSpecificData.toBytes(adv.vendorSpecificData[0]))
+                .isEqualTo(VENDOR_SPECIFIC_DATA_BYTES);
+
+        final String expectedString =
+                "DiscoveryAdvertisement: serviceUuid="
+                        + adv.serviceUuid
+                        + " uwbIndicationData={"
+                        + adv.uwbIndicationData
+                        + "} regulatoryInfo={"
+                        + adv.regulatoryInfo
+                        + "} firaProfileSupportInfo={"
+                        + adv.firaProfileSupportInfo
+                        + "} "
+                        + Arrays.toString(adv.vendorSpecificData);
+
+        assertThat(ADVERTISEMENT.toString()).isEqualTo(expectedString);
+    }
+
+    @Test
+    public void toBytes_succeedWithoutVendorData() {
+        assertThat(ADVERTISEMENT).isNotNull();
+
+        byte[] result =
+                DiscoveryAdvertisement.toBytes(ADVERTISEMENT, /*includeVendorSpecificData=*/ false);
+        assertThat(result).isEqualTo(BYTES_NO_VENDOR);
+    }
+
+    @Test
+    public void toBytes_succeedWithVendorData() {
+        assertThat(ADVERTISEMENT).isNotNull();
+
+        byte[] result =
+                DiscoveryAdvertisement.toBytes(ADVERTISEMENT, /*includeVendorSpecificData=*/ true);
+        assertThat(result).isEqualTo(BYTES);
+    }
+
+    @Test
+    public void getManufacturerSpecificDataInBytes_succeed() {
+        assertThat(ADVERTISEMENT).isNotNull();
+
+        byte[] result = DiscoveryAdvertisement.getManufacturerSpecificDataInBytes(ADVERTISEMENT);
+        byte[] expected = new byte[] {0x25, 0x75, 0x00, 0x10, (byte) 0xFF, 0x00};
+        assertThat(result).isEqualTo(expected);
+    }
+
+    @Test
+    public void getManufacturerSpecificDataInBytes_noData() {
+        DiscoveryAdvertisement adv = DiscoveryAdvertisement.fromBytes(MIN_BYTES, null);
+        assertThat(adv).isNotNull();
+
+        assertThat(DiscoveryAdvertisement.getManufacturerSpecificDataInBytes(adv)).isNull();
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/discovery/info/ChannelPowerInfoTest.java b/service/tests/src/com/android/server/uwb/discovery/info/ChannelPowerInfoTest.java
new file mode 100644
index 0000000..6a36537
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/discovery/info/ChannelPowerInfoTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.discovery.info;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit test for {@link ChannelPowerInfo}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ChannelPowerInfoTest {
+
+    private static final byte[] BYTES = new byte[] {(byte) 0xE5, (byte) 0x9F};
+    private static final int FIRST_CHANNELS = 14;
+    private static final int NUMBER_OF_CHANNELS = 2;
+    private static final boolean IS_INDOOR = true;
+    private static final int AVERAGE_POWER_DBM = -97;
+
+    @Test
+    public void fromBytes_emptyData() {
+        assertThat(ChannelPowerInfo.fromBytes(new byte[] {})).isNull();
+    }
+
+    @Test
+    public void fromBytes_dataTooShort() {
+        assertThat(ChannelPowerInfo.fromBytes(new byte[] {0x0})).isNull();
+    }
+
+    @Test
+    public void fromBytes_succeed() {
+        ChannelPowerInfo info = ChannelPowerInfo.fromBytes(BYTES);
+        assertThat(info).isNotNull();
+
+        assertThat(info.firstChannel).isEqualTo(FIRST_CHANNELS);
+        assertThat(info.numOfChannels).isEqualTo(NUMBER_OF_CHANNELS);
+        assertThat(info.isIndoor).isEqualTo(IS_INDOOR);
+        assertThat(info.averagePowerLimitDbm).isEqualTo(AVERAGE_POWER_DBM);
+    }
+
+    @Test
+    public void toBytes_succeed() {
+        ChannelPowerInfo info =
+                new ChannelPowerInfo(
+                        FIRST_CHANNELS, NUMBER_OF_CHANNELS, IS_INDOOR, AVERAGE_POWER_DBM);
+        assertThat(info).isNotNull();
+
+        byte[] result = ChannelPowerInfo.toBytes(info);
+        assertThat(result.length).isEqualTo(BYTES.length);
+        assertThat(result).isEqualTo(BYTES);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/discovery/info/FiraProfileSupportInfoTest.java b/service/tests/src/com/android/server/uwb/discovery/info/FiraProfileSupportInfoTest.java
new file mode 100644
index 0000000..4b0f068
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/discovery/info/FiraProfileSupportInfoTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.discovery.info;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.discovery.info.FiraProfileSupportInfo.FiraProfile;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit test for {@link FiraProfileSupportInfo}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class FiraProfileSupportInfoTest {
+
+    private static final byte[] BYTES = new byte[] {0x1};
+    private static final FiraProfile[] PROFILES = new FiraProfile[] {FiraProfile.PACS};
+
+    @Test
+    public void fromBytes_emptyData() {
+        assertThat(FiraProfileSupportInfo.fromBytes(new byte[] {})).isNull();
+    }
+
+    @Test
+    public void fromBytes_noProfile() {
+        FiraProfileSupportInfo info =
+                FiraProfileSupportInfo.fromBytes(new byte[] {0x0, 0x0, 0x0, 0x0, 0x0});
+        assertThat(info).isNotNull();
+
+        assertThat(info.supportedFiraProfiles).isEqualTo(new FiraProfile[] {});
+    }
+
+    @Test
+    public void fromBytes_undefinedProfile() {
+        FiraProfileSupportInfo info =
+                FiraProfileSupportInfo.fromBytes(new byte[] {(byte) 0x80, 0x0, 0x8, 0x0, 0x1});
+        assertThat(info).isNotNull();
+
+        assertThat(info.supportedFiraProfiles).isEqualTo(PROFILES);
+    }
+
+    @Test
+    public void fromBytes_succeed() {
+        FiraProfileSupportInfo info = FiraProfileSupportInfo.fromBytes(new byte[] {0x0, 0x0, 0x1});
+        assertThat(info).isNotNull();
+
+        assertThat(info.supportedFiraProfiles).isEqualTo(PROFILES);
+    }
+
+    @Test
+    public void toBytes_noProfile() {
+        FiraProfileSupportInfo info = new FiraProfileSupportInfo(new FiraProfile[] {});
+        assertThat(info).isNotNull();
+
+        byte[] result = FiraProfileSupportInfo.toBytes(info);
+        assertThat(result).isEqualTo(new byte[] {});
+    }
+
+    @Test
+    public void toBytes_duplicateProfile() {
+        FiraProfileSupportInfo info =
+                new FiraProfileSupportInfo(
+                        new FiraProfile[] {FiraProfile.PACS, FiraProfile.PACS, FiraProfile.PACS});
+        assertThat(info).isNotNull();
+
+        byte[] result = FiraProfileSupportInfo.toBytes(info);
+        assertThat(result).isEqualTo(BYTES);
+    }
+
+    @Test
+    public void toBytes_succeed() {
+        FiraProfileSupportInfo info = new FiraProfileSupportInfo(PROFILES);
+        assertThat(info).isNotNull();
+
+        byte[] result = FiraProfileSupportInfo.toBytes(info);
+        assertThat(result).isEqualTo(BYTES);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/discovery/info/RegulatoryInfoTest.java b/service/tests/src/com/android/server/uwb/discovery/info/RegulatoryInfoTest.java
new file mode 100644
index 0000000..58a033b
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/discovery/info/RegulatoryInfoTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.discovery.info;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.discovery.info.RegulatoryInfo.SourceOfInfo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Unit test for {@link RegulatoryInfo}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RegulatoryInfoTest {
+
+    private static final byte[] TEST_BYTES =
+            new byte[] {0x41, 0x55, 0x53, 0x62, 0x1E, 0x3B, 0x7F, (byte) 0xD8, (byte) 0x9F};
+    private static final SourceOfInfo SOURCE_OF_INFO = SourceOfInfo.SATELLITE_NAVIGATION_SYSTEM;
+    private static final boolean TRANSMITTION_PERMITTED = true;
+    private static final String COUNTRY_CODE =
+            new String(new byte[] {0x55, 0x53}, StandardCharsets.UTF_8);
+    private static final int TIMESTAMP = 1646148479;
+    private static final int FIRST_CHANNELS = 13;
+    private static final int NUMBER_OF_CHANNELS = 4;
+    private static final boolean IS_INDOOR = false;
+    private static final int AVERAGE_POWER_DBM = -97;
+
+    @Test
+    public void fromBytes_emptyData() {
+        assertThat(RegulatoryInfo.fromBytes(new byte[] {})).isNull();
+    }
+
+    @Test
+    public void fromBytes_dataTooShort() {
+        assertThat(RegulatoryInfo.fromBytes(new byte[] {0x0, 0x1})).isNull();
+    }
+
+    @Test
+    public void fromBytes_invalidReservedField() {
+        byte[] bytes =
+                new byte[] {0x47, 0x55, 0x53, 0x7F, 0x3B, 0x1E, 0x62, (byte) 0xD8, (byte) 0x9F};
+        assertThat(RegulatoryInfo.fromBytes(bytes)).isNull();
+    }
+
+    @Test
+    public void fromBytes_invalidCountryCode() {
+        byte[] bytes =
+                new byte[] {0x47, 0x2b, 0x53, 0x7F, 0x3B, 0x1E, 0x62, (byte) 0xD8, (byte) 0x9F};
+        assertThat(RegulatoryInfo.fromBytes(bytes)).isNull();
+    }
+
+    @Test
+    public void fromBytes_succeed() {
+        RegulatoryInfo info = RegulatoryInfo.fromBytes(TEST_BYTES);
+        assertThat(info).isNotNull();
+
+        assertThat(info.sourceOfInfo).isEqualTo(SOURCE_OF_INFO);
+        assertThat(info.outdoorsTransmittionPermitted).isEqualTo(TRANSMITTION_PERMITTED);
+        assertThat(info.countryCode).isEqualTo(COUNTRY_CODE);
+        assertThat(info.timestampSecondsSinceEpoch).isEqualTo(TIMESTAMP);
+        for (ChannelPowerInfo i : info.channelPowerInfos) {
+            assertThat(i.firstChannel).isEqualTo(FIRST_CHANNELS);
+            assertThat(i.numOfChannels).isEqualTo(NUMBER_OF_CHANNELS);
+            assertThat(i.isIndoor).isEqualTo(IS_INDOOR);
+            assertThat(i.averagePowerLimitDbm).isEqualTo(AVERAGE_POWER_DBM);
+        }
+    }
+
+    @Test
+    public void toBytes_succeed() {
+        RegulatoryInfo info =
+                new RegulatoryInfo(
+                        SOURCE_OF_INFO,
+                        TRANSMITTION_PERMITTED,
+                        COUNTRY_CODE,
+                        TIMESTAMP,
+                        new ChannelPowerInfo[] {
+                            new ChannelPowerInfo(
+                                    FIRST_CHANNELS,
+                                    NUMBER_OF_CHANNELS,
+                                    IS_INDOOR,
+                                    AVERAGE_POWER_DBM)
+                        });
+        assertThat(info).isNotNull();
+
+        byte[] result = RegulatoryInfo.toBytes(info);
+        assertThat(result.length).isEqualTo(TEST_BYTES.length);
+        assertThat(result).isEqualTo(TEST_BYTES);
+    }
+
+    @Test
+    public void fromBytesAndToBytes_eachSourceOfInfo() {
+        testSourceOfInfo(SourceOfInfo.USER_DEFINED, (byte) 0x80);
+        testSourceOfInfo(SourceOfInfo.SATELLITE_NAVIGATION_SYSTEM, (byte) 0x40);
+        testSourceOfInfo(SourceOfInfo.CELLULAR_SYSTEM, (byte) 0x20);
+        testSourceOfInfo(SourceOfInfo.ANOTHER_FIRA_DEVICE, (byte) 0x10);
+    }
+
+    private void testSourceOfInfo(SourceOfInfo sourceOfInfo, byte sourceOfInfoByte) {
+        RegulatoryInfo info =
+                new RegulatoryInfo(
+                        sourceOfInfo,
+                        TRANSMITTION_PERMITTED,
+                        COUNTRY_CODE,
+                        TIMESTAMP,
+                        new ChannelPowerInfo[] {
+                            new ChannelPowerInfo(
+                                    FIRST_CHANNELS,
+                                    NUMBER_OF_CHANNELS,
+                                    IS_INDOOR,
+                                    AVERAGE_POWER_DBM)
+                        });
+        byte[] bytes =
+                new byte[] {
+                    (byte) (sourceOfInfoByte | 0x01),
+                    0x55,
+                    0x53,
+                    0x62,
+                    0x1E,
+                    0x3B,
+                    0x7F,
+                    (byte) 0xD8,
+                    (byte) 0x9F
+                };
+        byte[] bytesResult = RegulatoryInfo.toBytes(info);
+        RegulatoryInfo regulatoryInfoResult = RegulatoryInfo.fromBytes(bytes);
+        assertThat(regulatoryInfoResult).isNotNull();
+
+        assertThat(bytesResult).isEqualTo(bytes);
+        assertThat(regulatoryInfoResult.sourceOfInfo).isEqualTo(sourceOfInfo);
+        assertThat(regulatoryInfoResult.outdoorsTransmittionPermitted)
+                .isEqualTo(TRANSMITTION_PERMITTED);
+        assertThat(regulatoryInfoResult.countryCode).isEqualTo(COUNTRY_CODE);
+        assertThat(regulatoryInfoResult.timestampSecondsSinceEpoch).isEqualTo(TIMESTAMP);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/discovery/info/SecureComponentInfoTest.java b/service/tests/src/com/android/server/uwb/discovery/info/SecureComponentInfoTest.java
new file mode 100644
index 0000000..e315610
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/discovery/info/SecureComponentInfoTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.discovery.info;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit test for {@link SecureComponentInfo}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SecureComponentInfoTest {
+
+    private static final byte[] TEST_BYTES = new byte[] {(byte) 0xF1, 0x32};
+    private static final boolean STATIC_INDICATION = true;
+    private static final int SECID = 113;
+    private static final SecureComponentInfo.SecureComponentType SC_TYPE =
+            SecureComponentInfo.SecureComponentType.DISCRETE_EUICC_REMOVABLE;
+    private static final SecureComponentInfo.SecureComponentProtocolType SC_PROTOCOL_TYPE =
+            SecureComponentInfo.SecureComponentProtocolType.ISO_IEC_7816_4;
+
+    @Test
+    public void fromBytes_emptyData() {
+        assertThat(SecureComponentInfo.fromBytes(new byte[] {})).isNull();
+    }
+
+    @Test
+    public void fromBytes_dataTooShort() {
+        assertThat(SecureComponentInfo.fromBytes(new byte[] {0x01})).isNull();
+    }
+
+    @Test
+    public void fromBytes_invalidSecid() {
+        assertThat(SecureComponentInfo.fromBytes(new byte[] {0x01, 0x00})).isNull();
+    }
+
+    @Test
+    public void fromBytes_invalidSecureComponentType() {
+        assertThat(SecureComponentInfo.fromBytes(new byte[] {0x02, (byte) 0x80})).isNull();
+    }
+
+    @Test
+    public void fromBytes_invalidSecureComponentProtocolType() {
+        assertThat(SecureComponentInfo.fromBytes(new byte[] {0x02, (byte) 0x14})).isNull();
+    }
+
+    @Test
+    public void fromBytes_succeed() {
+        SecureComponentInfo info = SecureComponentInfo.fromBytes(TEST_BYTES);
+        assertThat(info).isNotNull();
+
+        assertThat(info.staticIndication).isEqualTo(STATIC_INDICATION);
+        assertThat(info.secid).isEqualTo(SECID);
+        assertThat(info.secureComponentType).isEqualTo(SC_TYPE);
+        assertThat(info.secureComponentProtocolType).isEqualTo(SC_PROTOCOL_TYPE);
+    }
+
+    @Test
+    public void toBytes_succeed() {
+        SecureComponentInfo info =
+                new SecureComponentInfo(STATIC_INDICATION, SECID, SC_TYPE, SC_PROTOCOL_TYPE);
+        assertThat(info).isNotNull();
+
+        byte[] result = SecureComponentInfo.toBytes(info);
+        assertThat(result.length).isEqualTo(TEST_BYTES.length);
+        assertThat(SecureComponentInfo.toBytes(info)).isEqualTo(TEST_BYTES);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/discovery/info/UwbIndicationDataTest.java b/service/tests/src/com/android/server/uwb/discovery/info/UwbIndicationDataTest.java
new file mode 100644
index 0000000..270f303
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/discovery/info/UwbIndicationDataTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.discovery.info;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit test for {@link UwbIndicationData}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UwbIndicationDataTest {
+
+    private static final byte[] TEST_BYTES = new byte[] {(byte) 0b00010100, (byte) 0x9C};
+    private static final byte[] TEST_BYTES2 =
+            new byte[] {(byte) 0b11101001, (byte) 0x9C, (byte) 0xF1, 0x11};
+    private static final byte[] TEST_BYTES3 =
+            new byte[] {(byte) 0b11101001, (byte) 0x9C, (byte) 0xF1, 0x11, (byte) 0x01, 0x11};
+    private static final boolean FIRA_UWB_SUPPORT = true;
+    private static final boolean ISO_14443_SUPPORT = true;
+    private static final boolean UWB_REG_INFO_AVAILABLE_IN_AD = true;
+    private static final boolean UWB_REG_INFO_AVAILABLE_IN_OOB = false;
+    private static final boolean FIRA_PROFILE_INFO_AVAILABLE_IN_AD = true;
+    private static final boolean FIRA_PROFILE_INFO_AVAILABLE_IN_OOB = false;
+    private static final boolean DUAL_GAP_ROLE_SUPPORT = true;
+    private static final int BT_RSSI_THRESHOLD_DBM = -100;
+
+    private static final boolean STATIC_INDICATION = true;
+    private static final int SECID = 113;
+    private static final SecureComponentInfo.SecureComponentType SC_TYPE =
+            SecureComponentInfo.SecureComponentType.ESE_NONREMOVABLE;
+    private static final SecureComponentInfo.SecureComponentProtocolType SC_PROTOCOL_TYPE =
+            SecureComponentInfo.SecureComponentProtocolType.FIRA_OOB_ADMINISTRATIVE_PROTOCOL;
+
+    @Test
+    public void fromBytes_emptyData() {
+        assertThat(UwbIndicationData.fromBytes(new byte[] {})).isNull();
+    }
+
+    @Test
+    public void fromBytes_dataTooShort() {
+        assertThat(UwbIndicationData.fromBytes(new byte[] {0x00})).isNull();
+    }
+
+    @Test
+    public void fromBytes_invalidReservedField() {
+        assertThat(UwbIndicationData.fromBytes(new byte[] {0x02, 0x55})).isNull();
+    }
+
+    @Test
+    public void fromBytes_noSecureComponentInfo() {
+        UwbIndicationData info = UwbIndicationData.fromBytes(TEST_BYTES);
+        assertThat(info).isNotNull();
+
+        assertThat(info.firaUwbSupport).isEqualTo(false);
+        assertThat(info.iso14443Support).isEqualTo(false);
+        assertThat(info.uwbRegulartoryInfoAvailableInAd).isEqualTo(false);
+        assertThat(info.uwbRegulartoryInfoAvailableInOob).isEqualTo(true);
+        assertThat(info.firaProfileInfoAvailableInAd).isEqualTo(false);
+        assertThat(info.firaProfileInfoAvailableInOob).isEqualTo(true);
+        assertThat(info.dualGapRoleSupport).isEqualTo(false);
+        assertThat(info.bluetoothRssiThresholdDbm).isEqualTo(BT_RSSI_THRESHOLD_DBM);
+        assertThat(info.secureComponentInfos.length).isEqualTo(0);
+    }
+
+    @Test
+    public void fromBytes_oneValidAndOneInvalidSecureComponentInfo() {
+        UwbIndicationData info = UwbIndicationData.fromBytes(TEST_BYTES3);
+        assertThat(info).isNotNull();
+
+        assertThat(info.firaUwbSupport).isEqualTo(FIRA_UWB_SUPPORT);
+        assertThat(info.iso14443Support).isEqualTo(ISO_14443_SUPPORT);
+        assertThat(info.uwbRegulartoryInfoAvailableInAd).isEqualTo(UWB_REG_INFO_AVAILABLE_IN_AD);
+        assertThat(info.uwbRegulartoryInfoAvailableInOob).isEqualTo(UWB_REG_INFO_AVAILABLE_IN_OOB);
+        assertThat(info.firaProfileInfoAvailableInAd).isEqualTo(FIRA_PROFILE_INFO_AVAILABLE_IN_AD);
+        assertThat(info.firaProfileInfoAvailableInOob)
+                .isEqualTo(FIRA_PROFILE_INFO_AVAILABLE_IN_OOB);
+        assertThat(info.dualGapRoleSupport).isEqualTo(DUAL_GAP_ROLE_SUPPORT);
+        assertThat(info.bluetoothRssiThresholdDbm).isEqualTo(BT_RSSI_THRESHOLD_DBM);
+        assertThat(info.secureComponentInfos.length).isEqualTo(1);
+
+        SecureComponentInfo i = info.secureComponentInfos[0];
+        assertThat(i.staticIndication).isEqualTo(STATIC_INDICATION);
+        assertThat(i.secid).isEqualTo(SECID);
+        assertThat(i.secureComponentType).isEqualTo(SC_TYPE);
+        assertThat(i.secureComponentProtocolType).isEqualTo(SC_PROTOCOL_TYPE);
+    }
+
+    @Test
+    public void toBytes_succeed() {
+        UwbIndicationData info =
+                new UwbIndicationData(
+                        FIRA_UWB_SUPPORT,
+                        ISO_14443_SUPPORT,
+                        UWB_REG_INFO_AVAILABLE_IN_AD,
+                        UWB_REG_INFO_AVAILABLE_IN_OOB,
+                        FIRA_PROFILE_INFO_AVAILABLE_IN_AD,
+                        FIRA_PROFILE_INFO_AVAILABLE_IN_OOB,
+                        DUAL_GAP_ROLE_SUPPORT,
+                        BT_RSSI_THRESHOLD_DBM,
+                        new SecureComponentInfo[] {
+                            new SecureComponentInfo(
+                                    STATIC_INDICATION, SECID, SC_TYPE, SC_PROTOCOL_TYPE)
+                        });
+        assertThat(info).isNotNull();
+
+        byte[] result = UwbIndicationData.toBytes(info);
+        assertThat(result.length).isEqualTo(TEST_BYTES2.length);
+        assertThat(UwbIndicationData.toBytes(info)).isEqualTo(TEST_BYTES2);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/discovery/info/VendorSpecificDataTest.java b/service/tests/src/com/android/server/uwb/discovery/info/VendorSpecificDataTest.java
new file mode 100644
index 0000000..24c9a53
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/discovery/info/VendorSpecificDataTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.discovery.info;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit test for {@link VendorSpecificData}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VendorSpecificDataTest {
+
+    private static final int ID = 117;
+    private static final byte[] DATA = new byte[] {0x10, 0x0A, (byte) 0x93, (byte) 0xFF};
+    private static final byte[] BYTES =
+            new byte[] {0x75, 0x00, 0x10, 0x0A, (byte) 0x93, (byte) 0xFF};
+
+    @Test
+    public void fromBytes_emptyData() {
+        assertThat(VendorSpecificData.fromBytes(new byte[] {})).isNull();
+    }
+
+    @Test
+    public void fromBytes_dataTooShort() {
+        assertThat(VendorSpecificData.fromBytes(new byte[] {0x01})).isNull();
+    }
+
+    @Test
+    public void fromBytes_succeed() {
+        VendorSpecificData info = VendorSpecificData.fromBytes(BYTES);
+        assertThat(info).isNotNull();
+
+        assertThat(info.vendorId).isEqualTo(ID);
+        assertThat(info.vendorData).isEqualTo(DATA);
+    }
+
+    @Test
+    public void toBytes_succeed() {
+        VendorSpecificData info = new VendorSpecificData(ID, DATA);
+        assertThat(info).isNotNull();
+
+        byte[] result = VendorSpecificData.toBytes(info);
+        assertThat(result).isEqualTo(BYTES);
+    }
+
+    @Test
+    public void toBytes_emptyData() {
+        VendorSpecificData info = new VendorSpecificData(ID, new byte[] {});
+        assertThat(info).isNotNull();
+
+        byte[] result = VendorSpecificData.toBytes(info);
+        assertThat(result).isEqualTo(new byte[] {0x75, 0x00});
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/multichip/UwbMultichipDataTest.java b/service/tests/src/com/android/server/uwb/multichip/UwbMultichipDataTest.java
new file mode 100644
index 0000000..cac17c1
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/multichip/UwbMultichipDataTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.multichip;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.multchip.UwbMultichipData;
+import com.android.uwb.resources.R;
+
+import com.google.uwb.support.multichip.ChipInfoParams;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * Unit tests for {@link com.android.server.uwb.multichip.UwbMultichipData}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class UwbMultichipDataTest {
+    @Rule
+    public TemporaryFolder mTempFolder = TemporaryFolder.builder().build();
+    private static final String ASSETS_DIR = "assets/";
+    private static final String NONEXISTENT_CONFIG_FILE = "doesNotExist.xml";
+    private static final String ONE_CHIP_CONFIG_FILE = "singleChipConfig.xml";
+    private static final String TWO_CHIP_CONFIG_FILE = "twoChipConfig.xml";
+    private static final String NO_POSITION_CONFIG_FILE = "noPositionConfig.xml";
+
+    @Mock
+    private Context mMockContext;
+    @Mock
+    private Resources mMockResources;
+
+    private UwbMultichipData mUwbMultichipData;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mMockContext.getResources()).thenReturn(mMockResources);
+        mUwbMultichipData = new UwbMultichipData(mMockContext);
+    }
+
+    @Test
+    public void testInitializeSingleChip() {
+        when(mMockResources.getBoolean(R.bool.config_isMultichip)).thenReturn(false);
+
+        mUwbMultichipData.initialize();
+        List<ChipInfoParams> chipInfos = mUwbMultichipData.getChipInfos();
+        assertThat(chipInfos).hasSize(1);
+        ChipInfoParams chipInfo  = chipInfos.get(0);
+        assertThat(chipInfo.getChipId()).isEqualTo(mUwbMultichipData.getDefaultChipId());
+        assertThat(chipInfo.getPositionX()).isEqualTo(0.0);
+        assertThat(chipInfo.getPositionY()).isEqualTo(0.0);
+        assertThat(chipInfo.getPositionZ()).isEqualTo(0.0);
+    }
+
+    @Test
+    public void testInitializeMultiChipButNoFilePath() {
+        when(mMockResources.getBoolean(R.bool.config_isMultichip)).thenReturn(true);
+
+        mUwbMultichipData.initialize();
+        List<ChipInfoParams> chipInfos = mUwbMultichipData.getChipInfos();
+        assertThat(chipInfos).hasSize(1);
+        ChipInfoParams chipInfo  = chipInfos.get(0);
+        assertThat(chipInfo.getChipId()).isEqualTo(mUwbMultichipData.getDefaultChipId());
+        assertThat(chipInfo.getPositionX()).isEqualTo(0.0);
+        assertThat(chipInfo.getPositionY()).isEqualTo(0.0);
+        assertThat(chipInfo.getPositionZ()).isEqualTo(0.0);
+    }
+
+    @Test
+    public void testInitializeMultiChipButFileDoesNotExist() {
+        when(mMockResources.getBoolean(R.bool.config_isMultichip)).thenReturn(true);
+        when(mMockResources.getString(R.string.config_multichipConfigPath))
+                .thenReturn(NONEXISTENT_CONFIG_FILE);
+
+        mUwbMultichipData.initialize();
+        List<ChipInfoParams> chipInfos = mUwbMultichipData.getChipInfos();
+        assertThat(chipInfos).hasSize(1);
+        ChipInfoParams chipInfo  = chipInfos.get(0);
+        assertThat(chipInfo.getChipId()).isEqualTo(mUwbMultichipData.getDefaultChipId());
+        assertThat(chipInfo.getPositionX()).isEqualTo(0.0);
+        assertThat(chipInfo.getPositionY()).isEqualTo(0.0);
+        assertThat(chipInfo.getPositionZ()).isEqualTo(0.0);
+    }
+
+    @Test
+    public void testInitializeMultiChipOneChipConfig() throws Exception {
+        when(mMockResources.getBoolean(R.bool.config_isMultichip)).thenReturn(true);
+        when(mMockResources.getString(R.string.config_multichipConfigPath))
+                .thenReturn(createFileFromResource(ONE_CHIP_CONFIG_FILE)
+                        .getCanonicalPath());
+
+        mUwbMultichipData.initialize();
+
+        List<ChipInfoParams> chipInfos = mUwbMultichipData.getChipInfos();
+        assertThat(chipInfos).hasSize(1);
+        ChipInfoParams chipInfo  = chipInfos.get(0);
+        assertThat(chipInfo.getChipId()).isEqualTo("chipIdString");
+        assertThat(chipInfo.getPositionX()).isEqualTo(1.0);
+        assertThat(chipInfo.getPositionY()).isEqualTo(2.0);
+        assertThat(chipInfo.getPositionZ()).isEqualTo(3.0);
+    }
+
+    @Test
+    public void testInitializeMultiChipNoPosition() throws Exception {
+        when(mMockResources.getBoolean(R.bool.config_isMultichip)).thenReturn(true);
+        when(mMockResources.getString(R.string.config_multichipConfigPath))
+                .thenReturn(createFileFromResource(NO_POSITION_CONFIG_FILE)
+                        .getCanonicalPath());
+
+        mUwbMultichipData.initialize();
+
+        List<ChipInfoParams> chipInfos = mUwbMultichipData.getChipInfos();
+        assertThat(chipInfos).hasSize(1);
+        ChipInfoParams chipInfo  = chipInfos.get(0);
+        assertThat(chipInfo.getChipId()).isEqualTo("chipIdString");
+        assertThat(chipInfo.getPositionX()).isEqualTo(0.0);
+        assertThat(chipInfo.getPositionY()).isEqualTo(0.0);
+        assertThat(chipInfo.getPositionZ()).isEqualTo(0.0);
+    }
+
+    @Test
+    public void testInitializeMultiChipTwoChipConfig() throws Exception {
+        when(mMockResources.getBoolean(R.bool.config_isMultichip)).thenReturn(true);
+        when(mMockResources.getString(R.string.config_multichipConfigPath))
+                .thenReturn(createFileFromResource(TWO_CHIP_CONFIG_FILE)
+                        .getCanonicalPath());
+
+        mUwbMultichipData.initialize();
+
+        List<ChipInfoParams> chipInfos = mUwbMultichipData.getChipInfos();
+        assertThat(chipInfos).hasSize(2);
+
+        ChipInfoParams chipInfo  = chipInfos.get(0);
+        assertThat(chipInfo.getChipId()).isEqualTo("chipIdString1");
+        assertThat(chipInfo.getPositionX()).isEqualTo(1.0);
+        assertThat(chipInfo.getPositionY()).isEqualTo(2.0);
+        assertThat(chipInfo.getPositionZ()).isEqualTo(3.0);
+
+        chipInfo  = chipInfos.get(1);
+        assertThat(chipInfo.getChipId()).isEqualTo("chipIdString2");
+        assertThat(chipInfo.getPositionX()).isEqualTo(4.0);
+        assertThat(chipInfo.getPositionY()).isEqualTo(5.0);
+        assertThat(chipInfo.getPositionZ()).isEqualTo(6.0);
+    }
+
+    private File createFileFromResource(String configFile) throws Exception {
+        InputStream in = getClass().getClassLoader().getResourceAsStream(ASSETS_DIR + configFile);
+        File file = mTempFolder.newFile(configFile);
+
+        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+        FileOutputStream out = new FileOutputStream(file);
+
+        String line;
+
+        while ((line = reader.readLine()) != null) {
+            out.write(line.getBytes(StandardCharsets.UTF_8));
+        }
+
+        out.flush();
+        out.close();
+        return file;
+    }
+
+}
diff --git a/service/tests/src/com/android/server/uwb/params/CccDecoderTest.java b/service/tests/src/com/android/server/uwb/params/CccDecoderTest.java
new file mode 100644
index 0000000..f4d4332
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/params/CccDecoderTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.uwb.support.ccc.CccParams.CHAPS_PER_SLOT_3;
+import static com.google.uwb.support.ccc.CccParams.CHAPS_PER_SLOT_9;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_CONFIG_MODE_ADAPTIVE;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_CONFIG_MODE_CONTINUOUS;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_SEQUENCE_AES;
+import static com.google.uwb.support.ccc.CccParams.PULSE_SHAPE_PRECURSOR_FREE;
+import static com.google.uwb.support.ccc.CccParams.PULSE_SHAPE_PRECURSOR_FREE_SPECIAL;
+import static com.google.uwb.support.ccc.CccParams.UWB_CONFIG_0;
+
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.util.UwbUtil;
+
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.ccc.CccProtocolVersion;
+import com.google.uwb.support.ccc.CccPulseShapeCombo;
+import com.google.uwb.support.ccc.CccRangingStartedParams;
+import com.google.uwb.support.ccc.CccSpecificationParams;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/**
+ * Unit tests for {@link com.android.server.uwb.params.CccDecoder}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class CccDecoderTest {
+    private static final byte[] TEST_CCC_RANGING_OPENED_TLV_DATA =
+            UwbUtil.getByteArray("0a0402000100"
+                            + "a01001000200000000000000000000000000"
+                            + "a1080200010002000100"
+                            + "090402000100"
+                            + "140101");
+    private static final int TEST_CCC_RANGING_OPENED_TLV_NUM_PARAMS = 5;
+    public static final String TEST_CCC_SPECIFICATION_TLV_DATA_STRING =
+            "a00111"
+                    + "a10400000082"
+                    + "a20168"
+                    + "a30103"
+                    + "a4020102"
+                    + "a50100"
+                    + "a60112"
+                    + "a7040a000000";
+
+    private static final byte[] TEST_CCC_SPECIFICATION_TLV_DATA =
+            UwbUtil.getByteArray(TEST_CCC_SPECIFICATION_TLV_DATA_STRING);
+    public static final int TEST_CCC_SPECIFICATION_TLV_NUM_PARAMS = 8;
+    private final CccDecoder mCccDecoder = new CccDecoder();
+
+    private void verifyCccRangingOpend(CccRangingStartedParams cccRangingStartedParams) {
+        assertThat(cccRangingStartedParams).isNotNull();
+
+        assertThat(cccRangingStartedParams.getStartingStsIndex()).isEqualTo(0x00010002);
+        assertThat(cccRangingStartedParams.getHopModeKey()).isEqualTo(0x00020001);
+        assertThat(cccRangingStartedParams.getUwbTime0()).isEqualTo(0x0001000200010002L);
+        assertThat(cccRangingStartedParams.getRanMultiplier()).isEqualTo(0x00010002 / 96);
+    }
+
+    public static void verifyCccSpecification(CccSpecificationParams cccSpecificationParams) {
+        assertThat(cccSpecificationParams).isNotNull();
+
+        assertThat(cccSpecificationParams.getProtocolVersions()).isEqualTo(List.of(
+                CccProtocolVersion.fromBytes(new byte[] {1, 2}, 0)));
+        assertThat(cccSpecificationParams.getUwbConfigs()).isEqualTo(List.of(UWB_CONFIG_0));
+        assertThat(cccSpecificationParams.getPulseShapeCombos()).isEqualTo(
+                List.of(new CccPulseShapeCombo(
+                        PULSE_SHAPE_PRECURSOR_FREE, PULSE_SHAPE_PRECURSOR_FREE_SPECIAL)));
+        assertThat(cccSpecificationParams.getRanMultiplier()).isEqualTo(10);
+        assertThat(cccSpecificationParams.getChapsPerSlot()).isEqualTo(
+                List.of(CHAPS_PER_SLOT_3, CHAPS_PER_SLOT_9));
+        assertThat(cccSpecificationParams.getSyncCodes()).isEqualTo(
+                List.of(2, 8));
+        assertThat(cccSpecificationParams.getChannels()).isEqualTo(List.of(5, 9));
+        assertThat(cccSpecificationParams.getHoppingConfigModes()).isEqualTo(
+                List.of(HOPPING_CONFIG_MODE_CONTINUOUS, HOPPING_CONFIG_MODE_ADAPTIVE));
+        assertThat(cccSpecificationParams.getHoppingSequences()).isEqualTo(
+                List.of(HOPPING_SEQUENCE_AES));
+    }
+
+    @Test
+    public void testGetCccRangingOpened() throws Exception {
+        TlvDecoderBuffer tlvDecoderBuffer =
+                new TlvDecoderBuffer(
+                        TEST_CCC_RANGING_OPENED_TLV_DATA, TEST_CCC_RANGING_OPENED_TLV_NUM_PARAMS);
+        assertThat(tlvDecoderBuffer.parse()).isTrue();
+
+        CccRangingStartedParams cccRangingStartedParams = mCccDecoder.getParams(
+                tlvDecoderBuffer, CccRangingStartedParams.class);
+        verifyCccRangingOpend(cccRangingStartedParams);
+    }
+
+    @Test
+    public void testGetCccSpecification() throws Exception {
+        TlvDecoderBuffer tlvDecoderBuffer =
+                new TlvDecoderBuffer(
+                        TEST_CCC_SPECIFICATION_TLV_DATA, TEST_CCC_SPECIFICATION_TLV_NUM_PARAMS);
+        assertThat(tlvDecoderBuffer.parse()).isTrue();
+
+        CccSpecificationParams cccSpecificationParams = mCccDecoder.getParams(
+                tlvDecoderBuffer, CccSpecificationParams.class);
+        verifyCccSpecification(cccSpecificationParams);
+    }
+
+    @Test
+    public void testGetCccRangingOpenedViaTlvDecoder() throws Exception {
+        TlvDecoderBuffer tlvDecoderBuffer =
+                new TlvDecoderBuffer(
+                        TEST_CCC_RANGING_OPENED_TLV_DATA, TEST_CCC_RANGING_OPENED_TLV_NUM_PARAMS);
+        assertThat(tlvDecoderBuffer.parse()).isTrue();
+
+        CccRangingStartedParams cccRangingStartedParams = TlvDecoder
+                .getDecoder(CccParams.PROTOCOL_NAME)
+                .getParams(tlvDecoderBuffer, CccRangingStartedParams.class);
+        verifyCccRangingOpend(cccRangingStartedParams);
+    }
+
+    @Test
+    public void testGetCccSpecificationViaTlvDecoder() throws Exception {
+        TlvDecoderBuffer tlvDecoderBuffer =
+                new TlvDecoderBuffer(
+                        TEST_CCC_SPECIFICATION_TLV_DATA, TEST_CCC_SPECIFICATION_TLV_NUM_PARAMS);
+        assertThat(tlvDecoderBuffer.parse()).isTrue();
+
+        CccSpecificationParams cccSpecificationParams = TlvDecoder
+                .getDecoder(CccParams.PROTOCOL_NAME)
+                .getParams(tlvDecoderBuffer, CccSpecificationParams.class);
+        verifyCccSpecification(cccSpecificationParams);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/params/CccEncoderTest.java b/service/tests/src/com/android/server/uwb/params/CccEncoderTest.java
new file mode 100644
index 0000000..fcd2aa9
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/params/CccEncoderTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.uwb.support.ccc.CccParams.CHAPS_PER_SLOT_3;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_CONFIG_MODE_NONE;
+import static com.google.uwb.support.ccc.CccParams.HOPPING_SEQUENCE_DEFAULT;
+import static com.google.uwb.support.ccc.CccParams.PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE;
+import static com.google.uwb.support.ccc.CccParams.SLOTS_PER_ROUND_6;
+import static com.google.uwb.support.ccc.CccParams.UWB_CHANNEL_9;
+
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.util.UwbUtil;
+
+import com.google.uwb.support.ccc.CccOpenRangingParams;
+import com.google.uwb.support.ccc.CccParams;
+import com.google.uwb.support.ccc.CccPulseShapeCombo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Unit tests for {@link com.android.server.uwb.params.CccEncoder}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class CccEncoderTest {
+    private static final CccOpenRangingParams.Builder TEST_CCC_OPEN_RANGING_PARAMS =
+            new CccOpenRangingParams.Builder()
+                    .setProtocolVersion(CccParams.PROTOCOL_VERSION_1_0)
+                    .setUwbConfig(CccParams.UWB_CONFIG_0)
+                    .setPulseShapeCombo(
+                            new CccPulseShapeCombo(
+                                    PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE,
+                                    PULSE_SHAPE_SYMMETRICAL_ROOT_RAISED_COSINE))
+                    .setSessionId(1)
+                    .setRanMultiplier(4)
+                    .setChannel(UWB_CHANNEL_9)
+                    .setNumChapsPerSlot(CHAPS_PER_SLOT_3)
+                    .setNumResponderNodes(1)
+                    .setNumSlotsPerRound(SLOTS_PER_ROUND_6)
+                    .setSyncCodeIndex(1)
+                    .setHoppingConfigMode(HOPPING_CONFIG_MODE_NONE)
+                    .setHoppingSequence(HOPPING_SEQUENCE_DEFAULT);
+
+    private static final byte[] TEST_CCC_OPEN_RANGING_TLV_DATA =
+            UwbUtil.getByteArray("0001000201010401090501010904800100000E010011010103010"
+                    + "11B01062301012C0100A3020100A4020000A50100A602D0020802B004140101");
+
+    private final CccEncoder mCccEncoder = new CccEncoder();
+
+    @Test
+    public void testCccOpenRangingParams() throws Exception {
+        CccOpenRangingParams params = TEST_CCC_OPEN_RANGING_PARAMS.build();
+        TlvBuffer tlvs = mCccEncoder.getTlvBuffer(params);
+
+        assertThat(tlvs.getNoOfParams()).isEqualTo(17);
+        assertThat(tlvs.getByteArray()).isEqualTo(TEST_CCC_OPEN_RANGING_TLV_DATA);
+    }
+
+    @Test
+    public void testCccOpenRangingParamsViaTlvEncoder() throws Exception {
+        CccOpenRangingParams params = TEST_CCC_OPEN_RANGING_PARAMS.build();
+        TlvBuffer tlvs = TlvEncoder.getEncoder(CccParams.PROTOCOL_NAME).getTlvBuffer(params);
+
+        assertThat(tlvs.getNoOfParams()).isEqualTo(17);
+        assertThat(tlvs.getByteArray()).isEqualTo(TEST_CCC_OPEN_RANGING_TLV_DATA);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/params/FiraDecoderTest.java b/service/tests/src/com/android/server/uwb/params/FiraDecoderTest.java
new file mode 100644
index 0000000..4481067
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/params/FiraDecoderTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.uwb.support.fira.FiraParams.AoaCapabilityFlag.HAS_AZIMUTH_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.AoaCapabilityFlag.HAS_ELEVATION_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.AoaCapabilityFlag.HAS_FOM_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.AoaCapabilityFlag.HAS_FULL_AZIMUTH_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.AoaCapabilityFlag.HAS_INTERLEAVING_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.DeviceRoleCapabilityFlag.HAS_CONTROLEE_INITIATOR_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.DeviceRoleCapabilityFlag.HAS_CONTROLEE_RESPONDER_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.DeviceRoleCapabilityFlag.HAS_CONTROLLER_INITIATOR_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.DeviceRoleCapabilityFlag.HAS_CONTROLLER_RESPONDER_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.MultiNodeCapabilityFlag.HAS_ONE_TO_MANY_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.MultiNodeCapabilityFlag.HAS_UNICAST_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.PrfCapabilityFlag.HAS_BPRF_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.PrfCapabilityFlag.HAS_HPRF_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.PsduDataRateCapabilityFlag.HAS_27M2_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.PsduDataRateCapabilityFlag.HAS_31M2_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.PsduDataRateCapabilityFlag.HAS_6M81_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.PsduDataRateCapabilityFlag.HAS_7M80_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.RangingRoundCapabilityFlag.HAS_DS_TWR_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.RangingRoundCapabilityFlag.HAS_SS_TWR_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.RframeCapabilityFlag.HAS_SP0_RFRAME_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.RframeCapabilityFlag.HAS_SP1_RFRAME_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.RframeCapabilityFlag.HAS_SP3_RFRAME_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.StsCapabilityFlag.HAS_DYNAMIC_STS_SUPPORT;
+import static com.google.uwb.support.fira.FiraParams.StsCapabilityFlag.HAS_STATIC_STS_SUPPORT;
+
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.util.UwbUtil;
+
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraParams.BprfParameterSetCapabilityFlag;
+import com.google.uwb.support.fira.FiraParams.HprfParameterSetCapabilityFlag;
+import com.google.uwb.support.fira.FiraProtocolVersion;
+import com.google.uwb.support.fira.FiraSpecificationParams;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.EnumSet;
+import java.util.List;
+
+/**
+ * Unit tests for {@link com.android.server.uwb.params.FiraDecoder}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class FiraDecoderTest {
+    public static final String TEST_FIRA_SPECIFICATION_TLV_STRING =
+            "000401010102"
+                    + "010401050103"
+                    + "020103"
+                    + "03011F"
+                    + "040103"
+                    + "050103"
+                    + "060100"
+                    + "070100"
+                    + "080100"
+                    + "090101"
+                    + "0A0101"
+                    + "0B0109"
+                    + "0C010B"
+                    + "0D0103"
+                    + "0E0101"
+                    + "0F050000000003"
+                    + "10010F"
+                    + "110101"
+                    + "E30101";
+    private static final byte[] TEST_FIRA_SPECIFICATION_TLV_DATA =
+            UwbUtil.getByteArray(TEST_FIRA_SPECIFICATION_TLV_STRING);
+    public static final int TEST_FIRA_SPECIFICATION_TLV_NUM_PARAMS = 19;
+    private final FiraDecoder mFiraDecoder = new FiraDecoder();
+
+    public static void verifyFiraSpecification(FiraSpecificationParams firaSpecificationParams) {
+        assertThat(firaSpecificationParams).isNotNull();
+
+        assertThat(firaSpecificationParams.getMinPhyVersionSupported()).isEqualTo(
+                FiraProtocolVersion.fromBytes(new byte[] {1, 1},  0));
+        assertThat(firaSpecificationParams.getMaxPhyVersionSupported()).isEqualTo(
+                FiraProtocolVersion.fromBytes(new byte[] {1, 2},  0));
+        assertThat(firaSpecificationParams.getMinMacVersionSupported()).isEqualTo(
+                FiraProtocolVersion.fromBytes(new byte[] {1, 5},  0));
+        assertThat(firaSpecificationParams.getMaxMacVersionSupported()).isEqualTo(
+                FiraProtocolVersion.fromBytes(new byte[] {1, 3},  0));
+
+        assertThat(firaSpecificationParams.getDeviceRoleCapabilities()).isEqualTo(
+                EnumSet.of(HAS_CONTROLEE_RESPONDER_SUPPORT, HAS_CONTROLLER_RESPONDER_SUPPORT,
+                        HAS_CONTROLEE_INITIATOR_SUPPORT, HAS_CONTROLLER_INITIATOR_SUPPORT));
+
+        assertThat(firaSpecificationParams.getRangingRoundCapabilities()).isEqualTo(
+                EnumSet.of(HAS_DS_TWR_SUPPORT, HAS_SS_TWR_SUPPORT));
+        assertThat(firaSpecificationParams.hasNonDeferredModeSupport()).isTrue();
+
+        assertThat(firaSpecificationParams.getStsCapabilities()).isEqualTo(
+                EnumSet.of(HAS_STATIC_STS_SUPPORT, HAS_DYNAMIC_STS_SUPPORT));
+
+        assertThat(firaSpecificationParams.getMultiNodeCapabilities()).isEqualTo(
+                EnumSet.of(HAS_ONE_TO_MANY_SUPPORT, HAS_UNICAST_SUPPORT));
+
+        assertThat(firaSpecificationParams.hasBlockStridingSupport()).isEqualTo(true);
+
+        assertThat(firaSpecificationParams.getSupportedChannels()).isEqualTo(List.of(5, 9));
+
+        assertThat(firaSpecificationParams.getRframeCapabilities()).isEqualTo(
+                EnumSet.of(HAS_SP0_RFRAME_SUPPORT, HAS_SP1_RFRAME_SUPPORT,
+                        HAS_SP3_RFRAME_SUPPORT));
+
+        assertThat(firaSpecificationParams.getPrfCapabilities()).isEqualTo(
+                EnumSet.of(HAS_BPRF_SUPPORT, HAS_HPRF_SUPPORT));
+        assertThat(firaSpecificationParams.getPsduDataRateCapabilities()).isEqualTo(
+                EnumSet.of(HAS_6M81_SUPPORT, HAS_7M80_SUPPORT, HAS_27M2_SUPPORT, HAS_31M2_SUPPORT));
+
+        assertThat(firaSpecificationParams.getAoaCapabilities()).isEqualTo(
+                EnumSet.of(HAS_AZIMUTH_SUPPORT, HAS_ELEVATION_SUPPORT, HAS_FULL_AZIMUTH_SUPPORT,
+                        HAS_FOM_SUPPORT, HAS_INTERLEAVING_SUPPORT));
+
+        assertThat(firaSpecificationParams.getBprfParameterSetCapabilities()).isEqualTo(
+                EnumSet.of(BprfParameterSetCapabilityFlag.HAS_SET_1_SUPPORT));
+
+        assertThat(firaSpecificationParams.getHprfParameterSetCapabilities()).isEqualTo(
+                EnumSet.of(HprfParameterSetCapabilityFlag.HAS_SET_1_SUPPORT,
+                        HprfParameterSetCapabilityFlag.HAS_SET_2_SUPPORT));
+    }
+
+    @Test
+    public void testGetFiraSpecification() throws Exception {
+        TlvDecoderBuffer tlvDecoderBuffer =
+                new TlvDecoderBuffer(
+                        TEST_FIRA_SPECIFICATION_TLV_DATA, TEST_FIRA_SPECIFICATION_TLV_NUM_PARAMS);
+        assertThat(tlvDecoderBuffer.parse()).isTrue();
+
+        FiraSpecificationParams firaSpecificationParams = mFiraDecoder.getParams(
+                tlvDecoderBuffer, FiraSpecificationParams.class);
+        verifyFiraSpecification(firaSpecificationParams);
+    }
+
+    @Test
+    public void testGetFiraSpecificationViaTlvDecoder() throws Exception {
+        TlvDecoderBuffer tlvDecoderBuffer =
+                new TlvDecoderBuffer(
+                        TEST_FIRA_SPECIFICATION_TLV_DATA, TEST_FIRA_SPECIFICATION_TLV_NUM_PARAMS);
+        assertThat(tlvDecoderBuffer.parse()).isTrue();
+
+        FiraSpecificationParams firaSpecificationParams = TlvDecoder
+                .getDecoder(FiraParams.PROTOCOL_NAME)
+                .getParams(tlvDecoderBuffer, FiraSpecificationParams.class);
+        verifyFiraSpecification(firaSpecificationParams);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/params/FiraEncoderTest.java b/service/tests/src/com/android/server/uwb/params/FiraEncoderTest.java
new file mode 100644
index 0000000..db4bdbc
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/params/FiraEncoderTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.uwb.support.fira.FiraParams.MULTI_NODE_MODE_UNICAST;
+import static com.google.uwb.support.fira.FiraParams.RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY;
+import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_ROLE_RESPONDER;
+import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_TYPE_CONTROLLER;
+import static com.google.uwb.support.fira.FiraParams.RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE;
+
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.uwb.UwbAddress;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.util.UwbUtil;
+
+import com.google.uwb.support.fira.FiraOpenSessionParams;
+import com.google.uwb.support.fira.FiraParams;
+import com.google.uwb.support.fira.FiraRangingReconfigureParams;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+
+/**
+ * Unit tests for {@link com.android.server.uwb.params.FiraEncoder}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class FiraEncoderTest {
+    private static final FiraOpenSessionParams.Builder TEST_FIRA_OPEN_SESSION_PARAMS =
+            new FiraOpenSessionParams.Builder()
+                    .setProtocolVersion(FiraParams.PROTOCOL_VERSION_1_1)
+                    .setSessionId(1)
+                    .setDeviceType(RANGING_DEVICE_TYPE_CONTROLLER)
+                    .setDeviceRole(RANGING_DEVICE_ROLE_RESPONDER)
+                    .setDeviceAddress(UwbAddress.fromBytes(new byte[] { 0x4, 0x6}))
+                    .setDestAddressList(Arrays.asList(UwbAddress.fromBytes(new byte[] { 0x4, 0x6})))
+                    .setMultiNodeMode(MULTI_NODE_MODE_UNICAST)
+                    .setRangingRoundUsage(RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE)
+                    .setVendorId(new byte[]{0x5, 0x78})
+                    .setStaticStsIV(new byte[]{0x1a, 0x55, 0x77, 0x47, 0x7e, 0x7d});
+
+    private static final byte[] TEST_FIRA_OPEN_SESSION_TLV_DATA =
+            UwbUtil.getByteArray("000101010101020100030100040109050101060206040702060408"
+                    + "0260090904C80000000B01000C01030D01010E01010F0200001002204E11010012010314010"
+                    + "A1501021601001701011B011E1C01001F01002301002401002501322601002702780528061A"
+                    + "5577477E7D2901012A0200002B04000000002C01002D01002E01012F01013004000000003101"
+                    + "00350101E30100E40100E50100");
+
+    private static final FiraRangingReconfigureParams.Builder TEST_FIRA_RECONFIGURE_PARAMS =
+            new FiraRangingReconfigureParams.Builder()
+                    .setBlockStrideLength(6)
+                    .setRangeDataNtfConfig(RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY)
+                    .setRangeDataProximityFar(6)
+                    .setRangeDataProximityNear(4);
+
+    private static final byte[] TEST_FIRA_RECONFIGURE_TLV_DATA =
+            UwbUtil.getByteArray("2D01060E01020F02040010020600");
+
+    private final FiraEncoder mFiraEncoder = new FiraEncoder();
+
+    @Test
+    public void testFiraOpenSesisonParams() throws Exception {
+        FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build();
+        TlvBuffer tlvs = mFiraEncoder.getTlvBuffer(params);
+
+        assertThat(tlvs.getNoOfParams()).isEqualTo(44);
+        assertThat(tlvs.getByteArray()).isEqualTo(TEST_FIRA_OPEN_SESSION_TLV_DATA);
+    }
+
+    @Test
+    public void testFiraRangingReconfigureParams() throws Exception {
+        FiraRangingReconfigureParams params = TEST_FIRA_RECONFIGURE_PARAMS.build();
+        TlvBuffer tlvs = mFiraEncoder.getTlvBuffer(params);
+
+        assertThat(tlvs.getNoOfParams()).isEqualTo(4);
+        assertThat(tlvs.getByteArray()).isEqualTo(TEST_FIRA_RECONFIGURE_TLV_DATA);
+    }
+
+    @Test
+    public void testFiraOpenSesisonParamsViaTlvEncoder() throws Exception {
+        FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build();
+        TlvBuffer tlvs = TlvEncoder.getEncoder(FiraParams.PROTOCOL_NAME).getTlvBuffer(params);
+
+        assertThat(tlvs.getNoOfParams()).isEqualTo(44);
+        assertThat(tlvs.getByteArray()).isEqualTo(TEST_FIRA_OPEN_SESSION_TLV_DATA);
+    }
+
+    @Test
+    public void testFiraRangingReconfigureParamsViaTlvEncoder() throws Exception {
+        FiraRangingReconfigureParams params = TEST_FIRA_RECONFIGURE_PARAMS.build();
+        TlvBuffer tlvs = TlvEncoder.getEncoder(FiraParams.PROTOCOL_NAME).getTlvBuffer(params);
+
+        assertThat(tlvs.getNoOfParams()).isEqualTo(4);
+        assertThat(tlvs.getByteArray()).isEqualTo(TEST_FIRA_RECONFIGURE_TLV_DATA);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/params/GenericDecoderTest.java b/service/tests/src/com/android/server/uwb/params/GenericDecoderTest.java
new file mode 100644
index 0000000..1605b90
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/params/GenericDecoderTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.util.UwbUtil;
+
+import com.google.uwb.support.generic.GenericSpecificationParams;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link com.android.server.uwb.params.GenericDecoder}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class GenericDecoderTest {
+    private static final byte[] TEST_GENERC_SPECIFICATION_TLV_DATA =
+            UwbUtil.getByteArray("C00101"
+                    + FiraDecoderTest.TEST_FIRA_SPECIFICATION_TLV_STRING
+                    + CccDecoderTest.TEST_CCC_SPECIFICATION_TLV_DATA_STRING);
+    private static final int TEST_GENERIC_SPECIFICATION_TLV_NUM_PARAMS = 1
+            + FiraDecoderTest.TEST_FIRA_SPECIFICATION_TLV_NUM_PARAMS
+            + CccDecoderTest.TEST_CCC_SPECIFICATION_TLV_NUM_PARAMS;
+
+    private final GenericDecoder mGenericDecoder = new GenericDecoder();
+
+    @Test
+    public void testGetGenericSpecification() throws Exception {
+        TlvDecoderBuffer tlvDecoderBuffer =
+                new TlvDecoderBuffer(
+                        TEST_GENERC_SPECIFICATION_TLV_DATA,
+                        TEST_GENERIC_SPECIFICATION_TLV_NUM_PARAMS);
+        assertThat(tlvDecoderBuffer.parse()).isTrue();
+
+        GenericSpecificationParams genericSpecificationParams = mGenericDecoder.getParams(
+                tlvDecoderBuffer, GenericSpecificationParams.class);
+        assertThat(genericSpecificationParams.hasPowerStatsSupport()).isTrue();
+        FiraDecoderTest.verifyFiraSpecification(
+                genericSpecificationParams.getFiraSpecificationParams());
+        CccDecoderTest.verifyCccSpecification(
+                genericSpecificationParams.getCccSpecificationParams());
+    }
+
+    @Test
+    public void testGetGenericSpecificationViaTlvDecoder() throws Exception {
+        TlvDecoderBuffer tlvDecoderBuffer =
+                new TlvDecoderBuffer(
+                        TEST_GENERC_SPECIFICATION_TLV_DATA,
+                        TEST_GENERIC_SPECIFICATION_TLV_NUM_PARAMS);
+        assertThat(tlvDecoderBuffer.parse()).isTrue();
+
+        GenericSpecificationParams genericSpecificationParams = mGenericDecoder.getParams(
+                tlvDecoderBuffer, GenericSpecificationParams.class);
+        assertThat(genericSpecificationParams.hasPowerStatsSupport()).isTrue();
+        FiraDecoderTest.verifyFiraSpecification(
+                genericSpecificationParams.getFiraSpecificationParams());
+        CccDecoderTest.verifyCccSpecification(
+                genericSpecificationParams.getCccSpecificationParams());
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/params/TlvDecoderBufferTest.java b/service/tests/src/com/android/server/uwb/params/TlvDecoderBufferTest.java
new file mode 100644
index 0000000..f788bb8
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/params/TlvDecoderBufferTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.params;
+
+import static com.android.server.uwb.params.TlvDecoderBuffer.Tlv;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.uwb.util.UwbUtil;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Unit tests for {@link com.android.server.uwb.params.TlvDecoderBuffer}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class TlvDecoderBufferTest {
+    private static final byte[] TEST_TLV_DATA =
+            UwbUtil.getByteArray("0001010101020301000401050501010602040007020800080260090904C"
+                    + "80000000C01010D01011101001201031401091501021B041E000000270208072806010"
+                    + "2030405062B04640000002C01002D01002E010F32020000");
+    private static final List<Tlv> TEST_TLVS = Arrays.asList(
+            new Tlv((byte) 0, (byte) 1, UwbUtil.getByteArray("01")),
+            new Tlv((byte) 1, (byte) 1, UwbUtil.getByteArray("02")),
+            new Tlv((byte) 3, (byte) 1, UwbUtil.getByteArray("00")),
+            new Tlv((byte) 4, (byte) 1, UwbUtil.getByteArray("05")),
+            new Tlv((byte) 5, (byte) 1, UwbUtil.getByteArray("01")),
+            new Tlv((byte) 6, (byte) 2, UwbUtil.getByteArray("0400")),
+            new Tlv((byte) 7, (byte) 2, UwbUtil.getByteArray("0800")),
+            new Tlv((byte) 8, (byte) 2, UwbUtil.getByteArray("6009")),
+            new Tlv((byte) 9, (byte) 4, UwbUtil.getByteArray("C8000000")),
+            new Tlv((byte) 12, (byte) 1, UwbUtil.getByteArray("01")),
+            new Tlv((byte) 13, (byte) 1, UwbUtil.getByteArray("01")),
+            new Tlv((byte) 17, (byte) 1, UwbUtil.getByteArray("00")),
+            new Tlv((byte) 18, (byte) 1, UwbUtil.getByteArray("03")),
+            new Tlv((byte) 20, (byte) 1, UwbUtil.getByteArray("09")),
+            new Tlv((byte) 21, (byte) 1, UwbUtil.getByteArray("02")),
+            new Tlv((byte) 27, (byte) 4, UwbUtil.getByteArray("1E000000")),
+            new Tlv((byte) 39, (byte) 2, UwbUtil.getByteArray("0807")),
+            new Tlv((byte) 40, (byte) 6, UwbUtil.getByteArray("010203040506")),
+            new Tlv((byte) 43, (byte) 4, UwbUtil.getByteArray("64000000")),
+            new Tlv((byte) 44, (byte) 1, UwbUtil.getByteArray("00")),
+            new Tlv((byte) 45, (byte) 1, UwbUtil.getByteArray("00")),
+            new Tlv((byte) 46, (byte) 1, UwbUtil.getByteArray("0F")),
+            new Tlv((byte) 50, (byte) 2, UwbUtil.getByteArray("0000")));
+    private static final int TEST_TLV_NUM_PARAMS = TEST_TLVS.size();
+
+    @Test
+    public void testTlvParse() throws Exception {
+        TlvDecoderBuffer tlvDecoderBuffer =
+                new TlvDecoderBuffer(TEST_TLV_DATA, TEST_TLV_NUM_PARAMS);
+        assertThat(tlvDecoderBuffer.parse()).isTrue();
+
+        Collection<Tlv> tlvsParsedList = tlvDecoderBuffer.getTlvs();
+        Set<Tlv> tlvsExpected = Set.copyOf(TEST_TLVS);
+        Set<Tlv> tlvsParsed = Set.copyOf(tlvsParsedList);
+        assertThat(tlvsExpected).isEqualTo(tlvsParsed);
+    }
+
+    @Test
+    public void testGetters() throws Exception {
+        TlvDecoderBuffer tlvDecoderBuffer =
+                new TlvDecoderBuffer(TEST_TLV_DATA, TEST_TLV_NUM_PARAMS);
+        assertThat(tlvDecoderBuffer.parse()).isTrue();
+
+        assertThat(tlvDecoderBuffer.getByte(1)).isEqualTo(0x2);
+        assertThat(tlvDecoderBuffer.getShort(8)).isEqualTo(0x0960);
+        assertThat(tlvDecoderBuffer.getInt(9)).isEqualTo(0x000000C8);
+        assertThat(tlvDecoderBuffer.getByteArray(40)).isEqualTo(UwbUtil.getByteArray(
+                "010203040506"));
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/profile/ServiceProfileTest.java b/service/tests/src/com/android/server/uwb/profile/ServiceProfileTest.java
new file mode 100644
index 0000000..8fdea97
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/profile/ServiceProfileTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.profile;
+
+import static com.google.uwb.support.fira.FiraParams.PACS_PROFILE_SERVICE_ID;
+
+import static org.junit.Assert.assertEquals;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.uwb.support.profile.ServiceProfile;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ServiceProfileTest {
+    @Test
+    public void testServiceProfile() {
+        int serviceID = PACS_PROFILE_SERVICE_ID;
+
+        ServiceProfile config =
+                new ServiceProfile.Builder()
+                        .setServiceID(serviceID)
+                        .build();
+
+        assertEquals(config.getServiceID(), serviceID);
+
+        ServiceProfile fromBundle = ServiceProfile.fromBundle(config.toBundle());
+
+        assertEquals(fromBundle.getBundleVersion(), config.getBundleVersion());
+        assertEquals(fromBundle.getServiceID(), serviceID);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/SecureElementChannelTest.java b/service/tests/src/com/android/server/uwb/secure/SecureElementChannelTest.java
new file mode 100644
index 0000000..5d3030f
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/SecureElementChannelTest.java
@@ -0,0 +1,314 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.server.uwb.secure.iso7816.CommandApdu;
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.omapi.OmapiConnection;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+
+public class SecureElementChannelTest {
+    @Mock private OmapiConnection mMockOmapiConnection;
+    @Mock private OmapiConnection.InitCompletionCallback mInitCompletionCallback;
+    @Mock private CommandApdu mMockCommandApdu;
+    @Mock private ResponseApdu mMockResponseApdu;
+
+    @Captor
+    private ArgumentCaptor<OmapiConnection.InitCompletionCallback>
+            mInitCompletionCallbackCaptor;
+
+    private SecureElementChannel mSecureElementChannel;
+
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mSecureElementChannel =
+                new SecureElementChannel(mMockOmapiConnection);
+    }
+
+    @Test
+    public void init_callsInitOnOmapiConnection() {
+        doNothing().when(mMockOmapiConnection).init(mInitCompletionCallbackCaptor.capture());
+
+        mSecureElementChannel.init(mInitCompletionCallback);
+        mInitCompletionCallbackCaptor.getValue().onInitCompletion();
+
+        verify(mMockOmapiConnection).init(any());
+        verify(mInitCompletionCallback).onInitCompletion();
+    }
+
+    @Test
+    public void openChannel_getsSuccessResponse_success() throws Exception {
+        when(mMockOmapiConnection.openChannel()).thenReturn(ResponseApdu.SW_SUCCESS_APDU);
+
+        boolean result = mSecureElementChannel.openChannel();
+
+        verify(mMockOmapiConnection).openChannel();
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void openChannel_getsErrorResponse_returnsFalse() throws Exception {
+        when(mMockOmapiConnection.openChannel()).thenReturn(ResponseApdu.SW_UNKNOWN_APDU);
+
+        boolean result = mSecureElementChannel.openChannel();
+
+        verify(mMockOmapiConnection).openChannel();
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void openChannel_getsException_returnsFalse() throws Exception {
+        doThrow(new IOException()).when(mMockOmapiConnection).openChannel();
+
+        boolean result = mSecureElementChannel.openChannel();
+
+        verify(mMockOmapiConnection).openChannel();
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void openChannel_swTemporarilyUnavailableOnFirstTwoAttempts_succeedsOnThirdTry()
+            throws Exception {
+        init();
+        when(mMockOmapiConnection.openChannel())
+                .thenReturn(ResponseApdu.fromStatusWord(StatusWord.SW_CONDITIONS_NOT_SATISFIED))
+                .thenReturn(ResponseApdu.fromStatusWord(StatusWord.SW_CONDITIONS_NOT_SATISFIED))
+                .thenReturn(ResponseApdu.fromStatusWord(StatusWord.SW_NO_ERROR));
+
+        boolean result = mSecureElementChannel.openChannel();
+
+        verify(mMockOmapiConnection, times(3)).openChannel();
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void openChannel_swTemporarilyUnavailableAndNoSpecificDiagnostic_succeedsOnThirdTry()
+            throws Exception {
+        init();
+        when(mMockOmapiConnection.openChannel())
+                .thenReturn(ResponseApdu.fromStatusWord(StatusWord.SW_CONDITIONS_NOT_SATISFIED))
+                .thenReturn(ResponseApdu.fromStatusWord(StatusWord.SW_NO_SPECIFIC_DIAGNOSTIC))
+                .thenReturn(ResponseApdu.fromStatusWord(StatusWord.SW_NO_ERROR));
+
+        boolean result = mSecureElementChannel.openChannel();
+
+        verify(mMockOmapiConnection, times(3)).openChannel();
+
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void transmit_swTemporarilyUnavailableOnFirstTwoAttempts_succeedsOnThirdTry()
+            throws Exception {
+        init();
+        when(mMockOmapiConnection.openChannel()).thenReturn(ResponseApdu.SW_SUCCESS_APDU);
+        when(mMockOmapiConnection.transmit(eq(mMockCommandApdu)))
+                .thenReturn(ResponseApdu.fromStatusWord(StatusWord.SW_CONDITIONS_NOT_SATISFIED))
+                .thenReturn(ResponseApdu.fromStatusWord(StatusWord.SW_CONDITIONS_NOT_SATISFIED))
+                .thenReturn(mMockResponseApdu);
+        mSecureElementChannel.openChannel();
+
+        ResponseApdu actualResponse = mSecureElementChannel.transmit(mMockCommandApdu);
+
+        verify(mMockOmapiConnection, times(3)).transmit(eq(mMockCommandApdu));
+        assertThat(actualResponse).isEqualTo(mMockResponseApdu);
+    }
+
+    @Test
+    public void openChannel_retriesExhausted_failure() throws Exception {
+        init();
+        ResponseApdu responseApdu =
+                ResponseApdu.fromStatusWord(StatusWord.SW_CONDITIONS_NOT_SATISFIED);
+        when(mMockOmapiConnection.openChannel()).thenReturn(responseApdu);
+
+        boolean result = mSecureElementChannel.openChannel();
+
+        verify(mMockOmapiConnection, times(3)).openChannel();
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void openChannelWithResponse_unopened_success() throws IOException {
+        when(mMockOmapiConnection.openChannel()).thenReturn(ResponseApdu.SW_SUCCESS_APDU);
+
+        ResponseApdu response = mSecureElementChannel.openChannelWithResponse();
+
+        assertThat(response.getStatusWord()).isEqualTo(StatusWord.SW_NO_ERROR.toInt());
+    }
+
+
+    @Test
+    public void openChannelWithResponse_closed_success() throws IOException {
+        when(mMockOmapiConnection.openChannel()).thenReturn(ResponseApdu.SW_SUCCESS_APDU);
+        mSecureElementChannel.openChannelWithResponse();
+        mSecureElementChannel.closeChannel();
+
+        ResponseApdu response = mSecureElementChannel.openChannelWithResponse();
+
+        assertThat(response.getStatusWord()).isEqualTo(StatusWord.SW_NO_ERROR.toInt());
+    }
+
+    @Test
+    public void transmit_retriesExhausted_failure() throws Exception {
+        init();
+        when(mMockOmapiConnection.openChannel()).thenReturn(ResponseApdu.SW_SUCCESS_APDU);
+        when(mMockOmapiConnection.transmit(eq(mMockCommandApdu)))
+                .thenReturn(ResponseApdu.fromStatusWord(StatusWord.SW_CONDITIONS_NOT_SATISFIED));
+        mSecureElementChannel.openChannel();
+
+        ResponseApdu actualResponse = mSecureElementChannel.transmit(mMockCommandApdu);
+
+        verify(mMockOmapiConnection, times(3)).transmit(eq(mMockCommandApdu));
+        assertThat(actualResponse)
+                .isEqualTo(ResponseApdu.fromStatusWord(StatusWord.SW_CONDITIONS_NOT_SATISFIED));
+    }
+
+    @Test
+    public void closeChannel_unopened_success() throws IOException {
+        boolean result = mSecureElementChannel.closeChannel();
+
+        verify(mMockOmapiConnection).closeChannel();
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void closeChannel_opened_success() throws IOException {
+        when(mMockOmapiConnection.openChannel()).thenReturn(ResponseApdu.SW_SUCCESS_APDU);
+        mSecureElementChannel.openChannelWithResponse();
+
+        boolean result = mSecureElementChannel.closeChannel();
+
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void closeChannel_closed_success() throws IOException {
+        when(mMockOmapiConnection.openChannel()).thenReturn(ResponseApdu.SW_SUCCESS_APDU);
+        mSecureElementChannel.openChannelWithResponse();
+        mSecureElementChannel.closeChannel();
+
+        boolean result = mSecureElementChannel.closeChannel();
+
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void transmit_callsSeTransmit() throws Exception {
+        when(mMockOmapiConnection.openChannel()).thenReturn(ResponseApdu.SW_SUCCESS_APDU);
+        when(mMockOmapiConnection.transmit(any())).thenReturn(mMockResponseApdu);
+        mSecureElementChannel.openChannel();
+
+        ResponseApdu responseFromCall = mSecureElementChannel.transmit(mMockCommandApdu);
+
+        verify(mMockOmapiConnection).transmit(eq(mMockCommandApdu));
+        assertThat(responseFromCall).isEqualTo(mMockResponseApdu);
+    }
+
+    @Test
+    public void transmit_unopened_failure() throws IOException {
+        ResponseApdu response = mSecureElementChannel.transmit(mMockCommandApdu);
+
+        assertThat(response.getStatusWord())
+                .isEqualTo(StatusWord.SW_CONDITIONS_NOT_SATISFIED.toInt());
+    }
+
+    @Test
+    public void transmit_opened_success() throws IOException {
+        when(mMockOmapiConnection.openChannel()).thenReturn(ResponseApdu.SW_SUCCESS_APDU);
+        when(mMockOmapiConnection.transmit(any())).thenReturn(ResponseApdu.SW_SUCCESS_APDU);
+        mSecureElementChannel.openChannel();
+
+        ResponseApdu response = mSecureElementChannel.transmit(mMockCommandApdu);
+
+        assertThat(response.getStatusWord()).isEqualTo(StatusWord.SW_NO_ERROR.toInt());
+    }
+
+
+    @Test
+    public void transmit_closed_success() throws IOException {
+        when(mMockOmapiConnection.openChannel()).thenReturn(ResponseApdu.SW_SUCCESS_APDU);
+        mSecureElementChannel.openChannelWithResponse();
+        mSecureElementChannel.closeChannel();
+
+        ResponseApdu response = mSecureElementChannel.transmit(mMockCommandApdu);
+
+        assertThat(response.getStatusWord())
+                .isEqualTo(StatusWord.SW_CONDITIONS_NOT_SATISFIED.toInt());
+    }
+
+    @Test
+    public void isOpened_unopened_verifyResult() {
+        assertThat(mSecureElementChannel.isOpened()).isFalse();
+    }
+
+    @Test
+    public void isOpened_opened_verifyResult() throws IOException {
+        when(mMockOmapiConnection.openChannel()).thenReturn(ResponseApdu.SW_SUCCESS_APDU);
+
+        mSecureElementChannel.openChannel();
+
+        assertThat(mSecureElementChannel.isOpened()).isTrue();
+    }
+
+    @Test
+    public void isOpened_openFailed_verifyResult() throws IOException {
+        when(mMockOmapiConnection.openChannel()).thenReturn(ResponseApdu.SW_UNKNOWN_APDU);
+
+        mSecureElementChannel.openChannel();
+
+        assertThat(mSecureElementChannel.isOpened()).isFalse();
+    }
+
+    @Test
+    public void isOpened_closed_verifyResult() throws IOException {
+        when(mMockOmapiConnection.openChannel()).thenReturn(ResponseApdu.SW_SUCCESS_APDU);
+
+        mSecureElementChannel.openChannel();
+        mSecureElementChannel.closeChannel();
+
+        assertThat(mSecureElementChannel.isOpened()).isFalse();
+    }
+
+    private void init() {
+        mSecureElementChannel =
+                new SecureElementChannel(
+                        mMockOmapiConnection,
+                        /* removeDelayBetweenRetriesForTest= */ true);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/CsmlUtilTest.java b/service/tests/src/com/android/server/uwb/secure/csml/CsmlUtilTest.java
new file mode 100644
index 0000000..898a559
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/CsmlUtilTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+import com.android.server.uwb.util.ObjectIdentifier;
+
+import org.junit.Test;
+
+public class CsmlUtilTest {
+    @Test
+    public void encodeObjectIdentifierAsTlv() {
+        ObjectIdentifier oid =
+                ObjectIdentifier.fromBytes(new byte[]{(byte) 0x01, (byte) 0x02});
+        byte[] actual = CsmlUtil.encodeObjectIdentifierAsTlv(oid).toBytes();
+        byte[] expected = DataTypeConversionUtil.hexStringToByteArray("06020102");
+
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void constructGetDoTlvUsingTagList() {
+        TlvDatum.Tag doTag = new TlvDatum.Tag((byte) 0x0A);
+        byte[] actual = CsmlUtil.constructGetDoTlv(doTag).toBytes();
+        byte[] expected = DataTypeConversionUtil.hexStringToByteArray("5C010A");
+
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void constructTerminateSessionGetDoTlv() {
+        byte[] actual = CsmlUtil.constructTerminateSessionGetDoTlv().toBytes();
+        byte[] expected = DataTypeConversionUtil.hexStringToByteArray("4D05BF79028000");
+
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void constructDeepestTagOfGetDoPartContent() {
+        TlvDatum.Tag tag = new TlvDatum.Tag((byte) 0x0A);
+        int len = 2;
+        byte[] actual = CsmlUtil.constructDeepestTagOfGetDoPartContent(tag, len);
+        byte[] expected = new byte[] {(byte) 0x0A, (byte) 0x02};
+
+        assertThat(actual).isEqualTo(expected);
+    }
+
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/DeleteAdfCommandTest.java b/service/tests/src/com/android/server/uwb/secure/csml/DeleteAdfCommandTest.java
new file mode 100644
index 0000000..574c1a0
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/DeleteAdfCommandTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.util.DataTypeConversionUtil;
+import com.android.server.uwb.util.ObjectIdentifier;
+
+import org.junit.Test;
+
+/**
+ * Tests for DeleteAdfCommand
+ */
+public class DeleteAdfCommandTest {
+
+    @Test
+    public void encodeDeleteAdfCommand() {
+        ObjectIdentifier oid =
+                ObjectIdentifier.fromBytes(DataTypeConversionUtil.hexStringToByteArray("0102"));
+        // <code>cla | ins | p1 | p2 | lc | data | le</code>
+        byte[] expectedApdu = DataTypeConversionUtil.hexStringToByteArray(
+                "80E40000040602010200");
+        byte[] actualApdu = DeleteAdfCommand.build(oid)
+                .getCommandApdu().getEncoded();
+
+        assertThat(actualApdu).isEqualTo(expectedApdu);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/DeleteAdfResponseTest.java b/service/tests/src/com/android/server/uwb/secure/csml/DeleteAdfResponseTest.java
new file mode 100644
index 0000000..a5cc11a
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/DeleteAdfResponseTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.StatusWord;
+
+import org.junit.Test;
+
+public class DeleteAdfResponseTest {
+    @Test
+    public void successResponse() {
+        ResponseApdu responseApdu = ResponseApdu.fromStatusWord(StatusWord.SW_NO_ERROR);
+        DeleteAdfResponse deleteAdfResponse = DeleteAdfResponse.fromResponseApdu(responseApdu);
+
+        assertThat(deleteAdfResponse.isSuccess()).isTrue();
+    }
+
+    @Test
+    public void errorResponse() {
+        ResponseApdu responseApdu =
+                ResponseApdu.fromStatusWord(StatusWord.SW_WARNING_STATE_UNCHANGED);
+        DeleteAdfResponse deleteAdfResponse = DeleteAdfResponse.fromResponseApdu(responseApdu);
+
+        assertThat(deleteAdfResponse.isSuccess()).isFalse();
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/DispatchCommandTest.java b/service/tests/src/com/android/server/uwb/secure/csml/DispatchCommandTest.java
new file mode 100644
index 0000000..21bbe34
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/DispatchCommandTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import org.junit.Test;
+
+/**
+ * Tests for DispatchCommand.
+ */
+public class DispatchCommandTest {
+    @Test
+    public void encodeDispatchCommand() {
+        byte[] dispatchData = DataTypeConversionUtil.hexStringToByteArray("0A0B");
+        // <code>cla | ins | p1 | p2 | lc | data | le</code>
+        byte[] expectedApdu = DataTypeConversionUtil.hexStringToByteArray(
+                "80C2000006710481020A0B00");
+        byte[] actualApdu = DispatchCommand.build(dispatchData)
+                .getCommandApdu().getEncoded();
+
+        assertThat(actualApdu).isEqualTo(expectedApdu);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/DispatchResponseTest.java b/service/tests/src/com/android/server/uwb/secure/csml/DispatchResponseTest.java
new file mode 100644
index 0000000..7d40218
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/DispatchResponseTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+import com.android.server.uwb.util.ObjectIdentifier;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Test;
+
+public class DispatchResponseTest {
+    @Test
+    public void validResponseWithTransactionSuccess() {
+        TlvDatum statusTlv = new TlvDatum(DispatchResponse.STATUS_TAG, new byte[] {(byte) 0x00});
+        TlvDatum responseTlv = new TlvDatum(FiRaResponse.PROPRIETARY_RESPONSE_TAG,
+                statusTlv);
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(
+                responseTlv.toBytes(),
+                StatusWord.SW_NO_ERROR.toInt());
+        DispatchResponse dispatchResponse = DispatchResponse.fromResponseApdu(responseApdu);
+
+        assertThat(dispatchResponse.notifications).hasSize(1);
+        assertThat(dispatchResponse.notifications.get(0).notificationEventId)
+                .isEqualTo(DispatchResponse.NOTIFICATION_EVENT_ID_SEURE_SESSION_AUTO_TERMINATED);
+        assertThat(dispatchResponse.getOutboundData().isPresent()).isFalse();
+    }
+
+    @Test
+    public void validResponseWithTransactionError() {
+        TlvDatum statusTlv = new TlvDatum(DispatchResponse.STATUS_TAG, new byte[] {(byte) 0xFF});
+        TlvDatum responseTlv = new TlvDatum(FiRaResponse.PROPRIETARY_RESPONSE_TAG,
+                statusTlv);
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(
+                responseTlv.toBytes(),
+                StatusWord.SW_NO_ERROR.toInt());
+        DispatchResponse dispatchResponse = DispatchResponse.fromResponseApdu(responseApdu);
+
+        assertThat(dispatchResponse.notifications).hasSize(1);
+        assertThat(dispatchResponse.notifications.get(0).notificationEventId)
+                .isEqualTo(DispatchResponse.NOTIFICATION_EVENT_ID_SECURE_SESSION_ABORTED);
+        assertThat(dispatchResponse.getOutboundData().isPresent()).isFalse();
+    }
+
+    @Test
+    public void validResponseWithOutboundDataToRemote() {
+        TlvDatum statusTlv = new TlvDatum(DispatchResponse.STATUS_TAG, new byte[] {(byte) 0x80});
+        TlvDatum dataTlv = new TlvDatum(DispatchResponse.DATA_TAG,
+                DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+        TlvDatum responseTlv = new TlvDatum(FiRaResponse.PROPRIETARY_RESPONSE_TAG,
+                Bytes.concat(statusTlv.toBytes(), dataTlv.toBytes()));
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(
+                responseTlv.toBytes(),
+                StatusWord.SW_NO_ERROR.toInt());
+        DispatchResponse dispatchResponse = DispatchResponse.fromResponseApdu(responseApdu);
+
+        assertThat(dispatchResponse.notifications).hasSize(0);
+        assertThat(dispatchResponse.getOutboundData().isPresent()).isTrue();
+        assertThat(dispatchResponse.getOutboundData().get().target)
+                .isEqualTo(DispatchResponse.OUTBOUND_TARGET_REMOTE);
+        assertThat(dispatchResponse.getOutboundData().get().data)
+                .isEqualTo(DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+    }
+
+    @Test
+    public void validResponseWithOutboundDataToHost() {
+        TlvDatum statusTlv = new TlvDatum(DispatchResponse.STATUS_TAG, new byte[] {(byte) 0x81});
+        TlvDatum dataTlv = new TlvDatum(DispatchResponse.DATA_TAG,
+                DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+        TlvDatum responseTlv = new TlvDatum(FiRaResponse.PROPRIETARY_RESPONSE_TAG,
+                Bytes.concat(statusTlv.toBytes(), dataTlv.toBytes()));
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(
+                responseTlv.toBytes(),
+                StatusWord.SW_NO_ERROR.toInt());
+        DispatchResponse dispatchResponse = DispatchResponse.fromResponseApdu(responseApdu);
+
+        assertThat(dispatchResponse.notifications).hasSize(0);
+        assertThat(dispatchResponse.getOutboundData().isPresent()).isTrue();
+        assertThat(dispatchResponse.getOutboundData().get().target)
+                .isEqualTo(DispatchResponse.OUTBOUND_TARGET_HOST_APP);
+        assertThat(dispatchResponse.getOutboundData().get().data)
+                .isEqualTo(DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+    }
+
+    @Test
+    public void validResponseWithAdfSelectedNotification() {
+        TlvDatum notiFormat = new TlvDatum(DispatchResponse.NOTIFICATION_FORMAT_TAG,
+                new byte[] {(byte) 0x00});
+        TlvDatum notiId = new TlvDatum(DispatchResponse.NOTIFICATION_EVENT_ID_TAG,
+                new byte[] { (byte) 0x00});
+        TlvDatum notiData = new TlvDatum(DispatchResponse.NOTIFICATION_DATA_TAG,
+                DataTypeConversionUtil.hexStringToByteArray("0000000100000002"));
+        TlvDatum notiTlv = new TlvDatum(DispatchResponse.NOTIFICATION_TAG,
+                Bytes.concat(notiFormat.toBytes(), notiId.toBytes(), notiData.toBytes()));
+        TlvDatum responseTlv = new TlvDatum(FiRaResponse.PROPRIETARY_RESPONSE_TAG,
+                notiTlv.toBytes());
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(
+                responseTlv.toBytes(),
+                StatusWord.SW_NO_ERROR.toInt());
+        DispatchResponse dispatchResponse = DispatchResponse.fromResponseApdu(responseApdu);
+
+
+        assertThat(dispatchResponse.notifications).hasSize(1);
+        assertThat(dispatchResponse.notifications.get(0).notificationEventId)
+                .isEqualTo(DispatchResponse.NOTIFICATION_EVENT_ID_ADF_SELECTED);
+        assertThat(((DispatchResponse.AdfSelectedNotification)
+                dispatchResponse.notifications.get(0)).adfOid)
+                .isEqualTo(ObjectIdentifier.fromBytes(
+                        DataTypeConversionUtil.hexStringToByteArray("0000000100000002")));
+
+    }
+
+    @Test
+    public void validResponseWithSecureSessionEstablishedNotification() {
+        TlvDatum notiFormat = new TlvDatum(DispatchResponse.NOTIFICATION_FORMAT_TAG,
+                new byte[] {(byte) 0x00});
+        TlvDatum notiId = new TlvDatum(DispatchResponse.NOTIFICATION_EVENT_ID_TAG,
+                new byte[] { (byte) 0x01});
+        TlvDatum notiTlv = new TlvDatum(DispatchResponse.NOTIFICATION_TAG,
+                Bytes.concat(notiFormat.toBytes(), notiId.toBytes()));
+        TlvDatum responseTlv = new TlvDatum(FiRaResponse.PROPRIETARY_RESPONSE_TAG,
+                notiTlv.toBytes());
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(
+                responseTlv.toBytes(),
+                StatusWord.SW_NO_ERROR.toInt());
+        DispatchResponse dispatchResponse = DispatchResponse.fromResponseApdu(responseApdu);
+
+        assertThat(dispatchResponse.notifications).hasSize(1);
+        assertThat(dispatchResponse.notifications.get(0).notificationEventId)
+                .isEqualTo(DispatchResponse.NOTIFICATION_EVENT_ID_SECURE_CHANNEL_ESTABLISHED);
+    }
+
+    @Test
+    public void validResponseWithRdsAvailableNotification() {
+        TlvDatum notiFormat = new TlvDatum(DispatchResponse.NOTIFICATION_FORMAT_TAG,
+                new byte[] {(byte) 0x00});
+        TlvDatum notiId = new TlvDatum(DispatchResponse.NOTIFICATION_EVENT_ID_TAG,
+                new byte[] { (byte) 0x02});
+        // sessionIdLen | sessionId | arbitrary_data_len | arbitrary data
+        TlvDatum notiData = new TlvDatum(DispatchResponse.NOTIFICATION_DATA_TAG,
+                DataTypeConversionUtil.hexStringToByteArray("020102020A0B"));
+        TlvDatum notiTlv = new TlvDatum(DispatchResponse.NOTIFICATION_TAG,
+                Bytes.concat(notiFormat.toBytes(), notiId.toBytes(), notiData.toBytes()));
+        TlvDatum responseTlv = new TlvDatum(FiRaResponse.PROPRIETARY_RESPONSE_TAG,
+                notiTlv.toBytes());
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(
+                responseTlv.toBytes(),
+                StatusWord.SW_NO_ERROR.toInt());
+        DispatchResponse dispatchResponse = DispatchResponse.fromResponseApdu(responseApdu);
+
+        assertThat(dispatchResponse.notifications).hasSize(1);
+        assertThat(dispatchResponse.notifications.get(0).notificationEventId)
+                .isEqualTo(DispatchResponse.NOTIFICATION_EVENT_ID_RDS_AVAILABLE);
+        assertThat(((DispatchResponse.RdsAvailableNotification)
+                dispatchResponse.notifications.get(0)).sessionId).isEqualTo(0x0102);
+        assertThat(((DispatchResponse.RdsAvailableNotification)
+                dispatchResponse.notifications.get(0)).arbitraryData.get())
+                .isEqualTo(DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+    }
+
+    @Test
+    public void wrongStatusWord() {
+        ResponseApdu responseApdu =
+                ResponseApdu.fromStatusWord(StatusWord.SW_CONDITIONS_NOT_SATISFIED);
+        DispatchResponse dispatchResponse = DispatchResponse.fromResponseApdu(responseApdu);
+
+        assertThat(dispatchResponse.isSuccess()).isFalse();
+        assertThat(dispatchResponse.notifications).hasSize(0);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/GetDoCommandTest.java b/service/tests/src/com/android/server/uwb/secure/csml/GetDoCommandTest.java
new file mode 100644
index 0000000..09c9246
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/GetDoCommandTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import org.junit.Test;
+
+/**
+ * Tests for GetDoCommand.
+ */
+public class GetDoCommandTest {
+    @Test
+    public void encodeGetDoCommandTest() {
+        TlvDatum queryDatum = new TlvDatum(new Tag((byte) 0x0A),
+                DataTypeConversionUtil.hexStringToByteArray("0C0D00"));
+        // <code>cla | ins | p1 | p2 | lc | data | le</code>
+        byte[] expectedApdu = DataTypeConversionUtil.hexStringToByteArray(
+                "00CB3FFF050A030C0D0000");
+        byte[] actualApdu = GetDoCommand.build(queryDatum)
+                .getCommandApdu().getEncoded();
+
+        assertThat(actualApdu).isEqualTo(expectedApdu);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/GetDoResponseTest.java b/service/tests/src/com/android/server/uwb/secure/csml/GetDoResponseTest.java
new file mode 100644
index 0000000..ce0d438
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/GetDoResponseTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import org.junit.Test;
+
+public class GetDoResponseTest {
+    @Test
+    public void successResponse() {
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(
+                DataTypeConversionUtil.hexStringToByteArray("0A0B"),
+                StatusWord.SW_NO_ERROR.toInt());
+        GetDoResponse getDoResponse =
+                GetDoResponse.fromResponseApdu(responseApdu);
+
+        assertThat(getDoResponse.isSuccess()).isTrue();
+        assertThat(getDoResponse.data.get()).isEqualTo(
+                DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+    }
+
+    @Test
+    public void errorResponse() {
+        ResponseApdu responseApdu =
+                ResponseApdu.fromStatusWord(StatusWord.SW_WARNING_STATE_UNCHANGED);
+        GetDoResponse getDoResponse =
+                GetDoResponse.fromResponseApdu(responseApdu);
+
+        assertThat(getDoResponse.isSuccess()).isFalse();
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/GetLocalDataCommandTest.java b/service/tests/src/com/android/server/uwb/secure/csml/GetLocalDataCommandTest.java
new file mode 100644
index 0000000..2cd168f
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/GetLocalDataCommandTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import org.junit.Test;
+
+public class GetLocalDataCommandTest {
+    @Test
+    public void encodeGetLocalDataCommand() {
+        byte p1 = (byte) 0x0A;
+        byte p2 = (byte) 0x0B;
+
+        // <code>cla | ins | p1 | p2 | lc | data | le</code>
+        byte[] expectedApdu = DataTypeConversionUtil.hexStringToByteArray(
+                "80CA0A0B00");
+        byte[] actualApdu = GetLocalDataCommand.build(p1, p2)
+                .getCommandApdu().getEncoded();
+
+        assertThat(actualApdu).isEqualTo(expectedApdu);
+    }
+
+    @Test
+    public void getPaList() {
+        byte[] expectedApdu = DataTypeConversionUtil.hexStringToByteArray(
+                "80CA00B000");
+        byte[] actualApdu = GetLocalDataCommand.getPaListCommand()
+                .getCommandApdu().getEncoded();
+
+        assertThat(actualApdu).isEqualTo(expectedApdu);
+    }
+
+    @Test
+    public void getFiraAppletCertificates() {
+        byte[] expectedApdu = DataTypeConversionUtil.hexStringToByteArray(
+                "80CABF2100");
+        byte[] actualApdu = GetLocalDataCommand.getFiRaAppletCertificatesCommand()
+                .getCommandApdu().getEncoded();
+
+        assertThat(actualApdu).isEqualTo(expectedApdu);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/GetLocalDataResponseTest.java b/service/tests/src/com/android/server/uwb/secure/csml/GetLocalDataResponseTest.java
new file mode 100644
index 0000000..bc83334
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/GetLocalDataResponseTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import org.junit.Test;
+
+public class GetLocalDataResponseTest {
+    @Test
+    public void successResponse() {
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(
+                DataTypeConversionUtil.hexStringToByteArray("0A0B"),
+                StatusWord.SW_NO_ERROR.toInt());
+        GetLocalDataResponse getLocalDataResponse =
+                GetLocalDataResponse.fromResponseApdu(responseApdu);
+
+        assertThat(getLocalDataResponse.isSuccess()).isTrue();
+        assertThat(getLocalDataResponse.data.get()).isEqualTo(
+                DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+    }
+
+    @Test
+    public void errorResponse() {
+        ResponseApdu responseApdu =
+                ResponseApdu.fromStatusWord(StatusWord.SW_WARNING_STATE_UNCHANGED);
+        GetLocalDataResponse getLocalDataResponse =
+                GetLocalDataResponse.fromResponseApdu(responseApdu);
+
+        assertThat(getLocalDataResponse.isSuccess()).isFalse();
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/InitiateTransactionCommandTest.java b/service/tests/src/com/android/server/uwb/secure/csml/InitiateTransactionCommandTest.java
new file mode 100644
index 0000000..4d76759
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/InitiateTransactionCommandTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.util.DataTypeConversionUtil;
+import com.android.server.uwb.util.ObjectIdentifier;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class InitiateTransactionCommandTest {
+    @Test
+    public void encodeUnicastInitiateTransactionCommand() {
+        List<ObjectIdentifier> adfOids = Arrays.asList(
+                ObjectIdentifier.fromBytes(new byte[] {(byte) 0x01, (byte) 0x02}),
+                ObjectIdentifier.fromBytes(new byte[] {(byte) 0x01, (byte) 0x03}));
+        // <code>cla | ins | p1 | p2 | lc | data | le</code>
+        byte[] expectedApdu = DataTypeConversionUtil.hexStringToByteArray(
+                "8012000008060201020602010300");
+        byte[] actualApdu = InitiateTransactionCommand.buildForUnicast(adfOids)
+                .getCommandApdu().getEncoded();
+
+        assertThat(actualApdu).isEqualTo(expectedApdu);
+    }
+
+    @Test
+    public void encodeMulticastInitiateTransactionCommand() {
+        List<ObjectIdentifier> adfOids = Arrays.asList(
+                ObjectIdentifier.fromBytes(new byte[] {(byte) 0x01, (byte) 0x02}),
+                ObjectIdentifier.fromBytes(new byte[] {(byte) 0x01, (byte) 0x03}));
+        int sessionId = 0x0A0B0C0D;
+        // <code>cla | ins | p1 | p2 | lc | data | le</code>
+        byte[] expectedApdu = DataTypeConversionUtil.hexStringToByteArray(
+                "801201000E80040A0B0C0D060201020602010300");
+        byte[] actualApdu = InitiateTransactionCommand.buildForMulticast(adfOids, sessionId)
+                .getCommandApdu().getEncoded();
+
+        assertThat(actualApdu).isEqualTo(expectedApdu);
+
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void setZeroAdfOids() {
+        InitiateTransactionCommand.buildForUnicast(new ArrayList<>());
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/InitiateTransactionResponseTest.java b/service/tests/src/com/android/server/uwb/secure/csml/InitiateTransactionResponseTest.java
new file mode 100644
index 0000000..13c4d98
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/InitiateTransactionResponseTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Test;
+
+public class InitiateTransactionResponseTest {
+    @Test
+    public void validResponse() {
+        TlvDatum statusTlv = new TlvDatum(InitiateTransactionResponse.STATUS_TAG,
+                DataTypeConversionUtil.hexStringToByteArray("80"));
+        TlvDatum dataTlv = new TlvDatum(InitiateTransactionResponse.DATA_TAG,
+                DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+        TlvDatum responseTlv = new TlvDatum(FiRaResponse.PROPRIETARY_RESPONSE_TAG,
+                Bytes.concat(statusTlv.toBytes(), dataTlv.toBytes()));
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(responseTlv.toBytes(),
+                StatusWord.SW_NO_ERROR.toInt());
+        InitiateTransactionResponse initiateTransactionResponse =
+                InitiateTransactionResponse.fromResponseApdu(responseApdu);
+
+        assertThat(initiateTransactionResponse.outboundDataToRemoteApplet.get()).isEqualTo(
+                DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+    }
+
+    @Test
+    public void wrongStatusWord() {
+        ResponseApdu responseApdu = ResponseApdu.fromStatusWord(
+                StatusWord.SW_NO_SPECIFIC_DIAGNOSTIC);
+        InitiateTransactionResponse initiateTransactionResponse =
+                InitiateTransactionResponse.fromResponseApdu(responseApdu);
+
+        assertThat(initiateTransactionResponse.outboundDataToRemoteApplet.isPresent()).isFalse();
+    }
+
+    @Test
+    public void wrongTopTag() {
+        TlvDatum statusTlv = new TlvDatum(InitiateTransactionResponse.STATUS_TAG,
+                DataTypeConversionUtil.hexStringToByteArray("80"));
+        TlvDatum dataTlv = new TlvDatum(InitiateTransactionResponse.DATA_TAG,
+                DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+        TlvDatum responseTlv = new TlvDatum(new TlvDatum.Tag((byte) 0x01),
+                Bytes.concat(statusTlv.toBytes(), dataTlv.toBytes()));
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(responseTlv.toBytes(),
+                StatusWord.SW_NO_ERROR.toInt());
+        InitiateTransactionResponse initiateTransactionResponse =
+                InitiateTransactionResponse.fromResponseApdu(responseApdu);
+
+        assertThat(initiateTransactionResponse.outboundDataToRemoteApplet.isPresent()).isFalse();
+    }
+
+    @Test
+    public void wrongStatusValue() {
+        TlvDatum statusTlv = new TlvDatum(InitiateTransactionResponse.STATUS_TAG,
+                DataTypeConversionUtil.hexStringToByteArray("00"));
+        TlvDatum dataTlv = new TlvDatum(InitiateTransactionResponse.DATA_TAG,
+                DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+        TlvDatum responseTlv = new TlvDatum(FiRaResponse.PROPRIETARY_RESPONSE_TAG,
+                Bytes.concat(statusTlv.toBytes(), dataTlv.toBytes()));
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(responseTlv.toBytes(),
+                StatusWord.SW_NO_ERROR.toInt());
+        InitiateTransactionResponse initiateTransactionResponse =
+                InitiateTransactionResponse.fromResponseApdu(responseApdu);
+
+        assertThat(initiateTransactionResponse.outboundDataToRemoteApplet.isPresent()).isFalse();
+    }
+
+    @Test
+    public void emptyOutboundData() {
+        TlvDatum statusTlv = new TlvDatum(InitiateTransactionResponse.STATUS_TAG,
+                DataTypeConversionUtil.hexStringToByteArray("80"));
+        TlvDatum responseTlv = new TlvDatum(FiRaResponse.PROPRIETARY_RESPONSE_TAG,
+                statusTlv.toBytes());
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(responseTlv.toBytes(),
+                StatusWord.SW_NO_ERROR.toInt());
+        InitiateTransactionResponse initiateTransactionResponse =
+                InitiateTransactionResponse.fromResponseApdu(responseApdu);
+
+        assertThat(initiateTransactionResponse.outboundDataToRemoteApplet.isPresent()).isFalse();
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/PutDoCommandTest.java b/service/tests/src/com/android/server/uwb/secure/csml/PutDoCommandTest.java
new file mode 100644
index 0000000..017da46
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/PutDoCommandTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import org.junit.Test;
+
+/**
+ * Tests for PutDoCommand.
+ */
+public class PutDoCommandTest {
+
+    @Test
+    public void encodePutDoCommand() {
+        TlvDatum.Tag doTag = new TlvDatum.Tag(DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+        byte[] doData = DataTypeConversionUtil.hexStringToByteArray("A0B0");
+        // <code>cla | ins | p1 | p2 | lc | data | le</code>
+        byte[] expectedApdu = DataTypeConversionUtil.hexStringToByteArray(
+                "00DB3FFF050A0B02A0B000");
+        byte[] actualApdu = PutDoCommand.build(doTag, doData)
+                .getCommandApdu().getEncoded();
+
+        assertThat(actualApdu).isEqualTo(expectedApdu);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/PutDoResponseTest.java b/service/tests/src/com/android/server/uwb/secure/csml/PutDoResponseTest.java
new file mode 100644
index 0000000..e541eee
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/PutDoResponseTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.StatusWord;
+
+import org.junit.Test;
+
+public class PutDoResponseTest {
+    @Test
+    public void successResponse() {
+        ResponseApdu responseApdu = ResponseApdu.fromStatusWord(StatusWord.SW_NO_ERROR);
+        PutDoResponse putDoResponse = PutDoResponse.fromResponseApdu(responseApdu);
+
+        assertThat(putDoResponse.isSuccess()).isTrue();
+    }
+
+    @Test
+    public void errorResponse() {
+        ResponseApdu responseApdu =
+                ResponseApdu.fromStatusWord(StatusWord.SW_WARNING_STATE_UNCHANGED);
+        PutDoResponse putDoResponse = PutDoResponse.fromResponseApdu(responseApdu);
+
+        assertThat(putDoResponse.isSuccess()).isFalse();
+    }
+
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/SelectAdfCommandTest.java b/service/tests/src/com/android/server/uwb/secure/csml/SelectAdfCommandTest.java
new file mode 100644
index 0000000..f14aa67
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/SelectAdfCommandTest.java
@@ -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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.util.DataTypeConversionUtil;
+import com.android.server.uwb.util.ObjectIdentifier;
+
+import org.junit.Test;
+
+public class SelectAdfCommandTest {
+    @Test
+    public void encodeSelectAdfCommand() {
+        ObjectIdentifier oid =
+                ObjectIdentifier.fromBytes(DataTypeConversionUtil.hexStringToByteArray("0102"));
+        // <code>cla | ins | p1 | p2 | lc | data | le</code>
+        byte[] expectedApdu = DataTypeConversionUtil.hexStringToByteArray(
+                "80A50400040602010200");
+        byte[] actualApdu = SelectAdfCommand.build(oid)
+                .getCommandApdu().getEncoded();
+
+        assertThat(actualApdu).isEqualTo(expectedApdu);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/SelectAdfResponseTest.java b/service/tests/src/com/android/server/uwb/secure/csml/SelectAdfResponseTest.java
new file mode 100644
index 0000000..1567f55
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/SelectAdfResponseTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.StatusWord;
+
+import org.junit.Test;
+
+public class SelectAdfResponseTest {
+    @Test
+    public void successResponse() {
+        ResponseApdu responseApdu = ResponseApdu.fromStatusWord(StatusWord.SW_NO_ERROR);
+        SelectAdfResponse selectAdfResponse = SelectAdfResponse.fromResponseApdu(responseApdu);
+
+        assertThat(selectAdfResponse.isSuccess()).isTrue();
+    }
+
+    @Test
+    public void errorResponse() {
+        ResponseApdu responseApdu =
+                ResponseApdu.fromStatusWord(StatusWord.SW_WARNING_STATE_UNCHANGED);
+        SelectAdfResponse selectAdfResponse = SelectAdfResponse.fromResponseApdu(responseApdu);
+
+        assertThat(selectAdfResponse.isSuccess()).isFalse();
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/SwapInAdfCommandTest.java b/service/tests/src/com/android/server/uwb/secure/csml/SwapInAdfCommandTest.java
new file mode 100644
index 0000000..b5737d0
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/SwapInAdfCommandTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import org.junit.Test;
+
+public class SwapInAdfCommandTest {
+    @Test
+    public void encodeSwapInAdfCommand() {
+        byte[] secureBlob = DataTypeConversionUtil.hexStringToByteArray("0A0B");
+        // <code>cla | ins | p1 | p2 | lc | data | le</code>
+        byte[] expectedApdu = DataTypeConversionUtil.hexStringToByteArray(
+                "8040000005DF51020A0B00");
+        byte[] actualApdu = SwapInAdfCommand.build(secureBlob)
+                .getCommandApdu().getEncoded();
+
+        assertThat(actualApdu).isEqualTo(expectedApdu);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/SwapInAdfResponseTest.java b/service/tests/src/com/android/server/uwb/secure/csml/SwapInAdfResponseTest.java
new file mode 100644
index 0000000..061f04a
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/SwapInAdfResponseTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import org.junit.Test;
+
+public class SwapInAdfResponseTest {
+    @Test
+    public void validResponseData() {
+        TlvDatum dataTlv = new TlvDatum(SwapInAdfResponse.SLOT_IDENTIFIER_TAG,
+                DataTypeConversionUtil.hexStringToByteArray("00000001"));
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(
+                dataTlv.toBytes(), StatusWord.SW_NO_ERROR.toInt());
+        SwapInAdfResponse swapInAdfResponse = SwapInAdfResponse.fromResponseApdu(responseApdu);
+
+        assertThat(swapInAdfResponse.isSuccess()).isTrue();
+        assertThat(swapInAdfResponse.slotIdentifier.get())
+                .isEqualTo(DataTypeConversionUtil.hexStringToByteArray("00000001"));
+    }
+
+    @Test
+    public void wrongStatusWord() {
+        ResponseApdu responseApdu = ResponseApdu.fromStatusWord(
+                StatusWord.SW_CONDITIONS_NOT_SATISFIED);
+        SwapInAdfResponse swapInAdfResponse = SwapInAdfResponse.fromResponseApdu(responseApdu);
+
+        assertThat(swapInAdfResponse.isSuccess()).isFalse();
+        assertThat(swapInAdfResponse.slotIdentifier.isEmpty()).isTrue();
+    }
+
+    @Test
+    public void wrongDataTag() {
+        TlvDatum dataTlv = new TlvDatum(new TlvDatum.Tag((byte) 0x01),
+                DataTypeConversionUtil.hexStringToByteArray("01010101"));
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(
+                dataTlv.toBytes(), StatusWord.SW_NO_ERROR.toInt());
+        SwapInAdfResponse swapInAdfResponse = SwapInAdfResponse.fromResponseApdu(responseApdu);
+
+        assertThat(swapInAdfResponse.isSuccess()).isTrue();
+        assertThat(swapInAdfResponse.slotIdentifier.isEmpty()).isTrue();
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/SwapOutAdfCommandTest.java b/service/tests/src/com/android/server/uwb/secure/csml/SwapOutAdfCommandTest.java
new file mode 100644
index 0000000..71095fc
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/SwapOutAdfCommandTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import org.junit.Test;
+
+public class SwapOutAdfCommandTest {
+    @Test
+    public void encodeSwapOutAdfCommand() {
+        byte[] slotId = DataTypeConversionUtil.hexStringToByteArray("0A0B");
+        // <code>cla | ins | p1 | p2 | lc | data | le</code>
+        byte[] expectedApdu = DataTypeConversionUtil.hexStringToByteArray(
+                "804001000406020A0B00");
+        byte[] actualApdu = SwapOutAdfCommand.build(slotId)
+                .getCommandApdu().getEncoded();
+
+        assertThat(actualApdu).isEqualTo(expectedApdu);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/SwapOutAdfResponseTest.java b/service/tests/src/com/android/server/uwb/secure/csml/SwapOutAdfResponseTest.java
new file mode 100644
index 0000000..4be8daa
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/SwapOutAdfResponseTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.StatusWord;
+
+import org.junit.Test;
+
+public class SwapOutAdfResponseTest {
+    @Test
+    public void successResponse() {
+        ResponseApdu responseApdu = ResponseApdu.fromStatusWord(StatusWord.SW_NO_ERROR);
+        SwapOutAdfResponse swapOutAdfResponse = SwapOutAdfResponse.fromResponseApdu(responseApdu);
+
+        assertThat(swapOutAdfResponse.isSuccess()).isTrue();
+    }
+
+    @Test
+    public void errorResponse() {
+        ResponseApdu responseApdu =
+                ResponseApdu.fromStatusWord(StatusWord.SW_DATA_NOT_FOUND);
+        SwapOutAdfResponse swapOutAdfResponse = SwapOutAdfResponse.fromResponseApdu(responseApdu);
+
+        assertThat(swapOutAdfResponse.isSuccess()).isFalse();
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/TunnelCommandTest.java b/service/tests/src/com/android/server/uwb/secure/csml/TunnelCommandTest.java
new file mode 100644
index 0000000..af5acb3
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/TunnelCommandTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import org.junit.Test;
+
+/**
+ * Tests for TunnelCommand.
+ */
+public class TunnelCommandTest {
+    @Test
+    public void encodeTunnelCommandTest() {
+        byte[] tunnelData = DataTypeConversionUtil.hexStringToByteArray("0A0B");
+        // <code>cla | ins | p1 | p2 | lc | data | le</code>
+        byte[] expectedApdu = DataTypeConversionUtil.hexStringToByteArray(
+                "8014000006710481020A0B00");
+        byte[] actualApdu = TunnelCommand.build(tunnelData)
+                .getCommandApdu().getEncoded();
+
+        assertThat(actualApdu).isEqualTo(expectedApdu);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/csml/TunnelResponseTest.java b/service/tests/src/com/android/server/uwb/secure/csml/TunnelResponseTest.java
new file mode 100644
index 0000000..8442c93
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/csml/TunnelResponseTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.csml;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.StatusWord;
+import com.android.server.uwb.secure.iso7816.TlvDatum;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import org.junit.Test;
+
+public class TunnelResponseTest {
+    @Test
+    public void validResponseData() {
+        TlvDatum subDataTlv = new TlvDatum(TunnelResponse.DATA_TAG,
+                DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+        TlvDatum dataTlv = new TlvDatum(FiRaResponse.PROPRIETARY_RESPONSE_TAG, subDataTlv);
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(
+                dataTlv.toBytes(), StatusWord.SW_NO_ERROR.toInt());
+        TunnelResponse tunnelResponse = TunnelResponse.fromResponseApdu(responseApdu);
+
+        assertThat(tunnelResponse.isSuccess()).isTrue();
+        assertThat(tunnelResponse.outboundDataOrApdu.get())
+                .isEqualTo(DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+    }
+
+    @Test
+    public void wrongStatusWord() {
+        ResponseApdu responseApdu = ResponseApdu.fromStatusWord(
+                StatusWord.SW_CONDITIONS_NOT_SATISFIED);
+        TunnelResponse tunnelResponse = TunnelResponse.fromResponseApdu(responseApdu);
+
+        assertThat(tunnelResponse.isSuccess()).isFalse();
+        assertThat(tunnelResponse.outboundDataOrApdu.isEmpty()).isTrue();
+    }
+
+    @Test
+    public void wrongTopTag() {
+        TlvDatum subDataTlv = new TlvDatum(TunnelResponse.DATA_TAG,
+                DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+        TlvDatum dataTlv = new TlvDatum(new TlvDatum.Tag((byte) 0x06), subDataTlv);
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(
+                dataTlv.toBytes(), StatusWord.SW_NO_ERROR.toInt());
+        TunnelResponse tunnelResponse = TunnelResponse.fromResponseApdu(responseApdu);
+
+        assertThat(tunnelResponse.isSuccess()).isTrue();
+        assertThat(tunnelResponse.outboundDataOrApdu.isEmpty()).isTrue();
+    }
+
+    @Test
+    public void wrongDataTag() {
+        TlvDatum subDataTlv = new TlvDatum(new TlvDatum.Tag((byte) 0x01),
+                DataTypeConversionUtil.hexStringToByteArray("0A0B"));
+        TlvDatum dataTlv = new TlvDatum(FiRaResponse.PROPRIETARY_RESPONSE_TAG, subDataTlv);
+        ResponseApdu responseApdu = ResponseApdu.fromDataAndStatusWord(
+                dataTlv.toBytes(), StatusWord.SW_NO_ERROR.toInt());
+        TunnelResponse tunnelResponse = TunnelResponse.fromResponseApdu(responseApdu);
+
+        assertThat(tunnelResponse.isSuccess()).isTrue();
+        assertThat(tunnelResponse.outboundDataOrApdu.isEmpty()).isTrue();
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/iso7816/CommandApduTest.java b/service/tests/src/com/android/server/uwb/secure/iso7816/CommandApduTest.java
new file mode 100644
index 0000000..7b08420
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/iso7816/CommandApduTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.
+ */
+package com.android.server.uwb.secure.iso7816;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.io.BaseEncoding;
+import com.google.common.primitives.Bytes;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/** Test for {@link CommandApdu}. */
+public class CommandApduTest {
+    private static BaseEncoding sHex = BaseEncoding.base16().lowerCase();
+
+    @Rule public ExpectedException thrown = ExpectedException.none();
+
+    @Test
+    public void testCommandApdu() {
+        StatusWord[] exp = {StatusWord.SW_NO_ERROR};
+        byte[] cdata = new byte[255];
+        generateData(cdata);
+        CommandApdu cmd = new CommandApdu(0, 1, 2, 3, cdata, 255, false, exp);
+        assertThat(cmd.getCla()).isEqualTo(0);
+        assertThat(cmd.getIns()).isEqualTo(1);
+        assertThat(cmd.getP1()).isEqualTo(2);
+        assertThat(cmd.getP2()).isEqualTo(3);
+        assertThat(cmd.getCommandData()).isEqualTo(cdata);
+        assertThat(cmd.getLe()).isEqualTo(255);
+    }
+
+    @Test
+    public void testCommandApdu_InvalidLc() {
+        thrown.expect(IllegalArgumentException.class);
+        new CommandApdu(0, 1, 2, 3, new byte[65566], 255, false, StatusWord.SW_NO_ERROR);
+    }
+
+    @Test
+    public void testCommandApdu_InvalidLe() {
+        thrown.expect(IllegalArgumentException.class);
+        new CommandApdu(0, 1, 2, 3, null, 65566, false, StatusWord.SW_NO_ERROR);
+    }
+
+    @Test
+    public void testGetEncoded_standard() {
+        StatusWord[] exp = {StatusWord.SW_NO_ERROR};
+        byte[] cdata = new byte[255];
+        generateData(cdata);
+        CommandApdu cmd = new CommandApdu(0, 1, 2, 3, cdata, -1, false, exp);
+
+        assertThat(cmd.getEncoded()).isEqualTo(Bytes.concat(sHex.decode("00010203ff"), cdata));
+    }
+
+    @Test
+    public void testGetEncoded_extended() {
+        StatusWord[] exp = {StatusWord.SW_NO_ERROR};
+        byte[] cdata = new byte[512];
+        generateData(cdata);
+        CommandApdu cmd = new CommandApdu(0, 1, 2, 3, cdata, -1, false, exp);
+
+        assertThat(cmd.getEncoded()).isEqualTo(Bytes.concat(sHex.decode("00010203000200"), cdata));
+    }
+
+    @Test
+    public void testExpected() {
+        StatusWord[] errorNoError =
+                new StatusWord[] {StatusWord.SW_NO_ERROR, StatusWord.SW_DATA_NOT_FOUND};
+        CommandApdu.Builder builder = CommandApdu.builder(0x80, 0xe2, 0x00, 0x00);
+
+        Set<StatusWord> noErrorSet = new HashSet<>();
+        noErrorSet.add(StatusWord.SW_NO_ERROR);
+        assertThat(noErrorSet).isEqualTo(builder.build().getExpected());
+
+        Set<StatusWord> dataNotFoundSet = new HashSet<>();
+        dataNotFoundSet.add(StatusWord.SW_DATA_NOT_FOUND);
+        Set<StatusWord> errorNoErrorSet = new HashSet<>(Arrays.asList(errorNoError));
+        CommandApdu[] cmds =
+                new CommandApdu[] {
+                        builder.setExpected(noErrorSet).build(),
+                        builder.setExpected(dataNotFoundSet).build(),
+                        builder.setExpected(errorNoErrorSet).build(),
+                        builder.setExpected(
+                                new StatusWord[] {StatusWord.SW_NO_ERROR}).build(),
+                        builder.setExpected(
+                                new StatusWord[] {StatusWord.SW_DATA_NOT_FOUND}).build(),
+                        builder.setExpected(errorNoError).build(),
+                };
+
+        StatusWord[][] expected =
+                new StatusWord[][] {
+                        {StatusWord.SW_NO_ERROR},
+                        {StatusWord.SW_DATA_NOT_FOUND},
+                        errorNoError,
+                        {StatusWord.SW_NO_ERROR},
+                        {StatusWord.SW_DATA_NOT_FOUND},
+                        errorNoError,
+                };
+
+        int i = 0;
+        for (CommandApdu cmd : cmds) {
+            // make sure that each command's expected set has exactly what we put in the builder.
+            Set<StatusWord> expectedSet = new HashSet<>(Arrays.asList(expected[i++]));
+            assertThat(cmd.getExpected()).isEqualTo(expectedSet);
+        }
+    }
+
+    @Test
+    public void testDoNotSetExpected() {
+        CommandApdu cmd = CommandApdu.builder(0, 0, 0, 0).build();
+        assertThat(cmd.getExpected()).hasSize(1);
+        assertThat(cmd.getExpected()).contains(StatusWord.SW_NO_ERROR);
+    }
+
+    @Test
+    public void testSetEmptyExpected() {
+        thrown.expect(IllegalArgumentException.class);
+        @SuppressWarnings("unused")
+        CommandApdu cmd =
+                CommandApdu.builder(0, 0, 0, 0)
+                        .setExpected(Collections.emptySet()).build();
+    }
+
+    @Test
+    public void testExtendedLe() {
+        CommandApdu apdu = CommandApdu.builder(0, 1, 2, 3)
+                .setExtendedLength().setLe(0).build();
+        assertThat(apdu.getEncoded()).isEqualTo(sHex.decode("00010203000000"));
+
+        apdu = CommandApdu.builder(0, 1, 2, 3)
+                .setExtendedLength().setLe(0x1234).build();
+        assertThat(apdu.getEncoded()).isEqualTo(sHex.decode("00010203001234"));
+    }
+
+    @Test
+    public void testUnknownExpected() {
+        thrown.expect(IllegalArgumentException.class);
+        @SuppressWarnings("unused")
+        CommandApdu cmd =
+                CommandApdu.builder(0x80, 0xe2, 0x00, 0x00)
+                        .setExpected(StatusWord.fromInt(0x1111)).build();
+    }
+
+    private void generateData(byte[] cdata) {
+        for (int i = 0; i < cdata.length; ++i) {
+            cdata[i] = (byte) (i % 255);
+        }
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/iso7816/ResponseApduTest.java b/service/tests/src/com/android/server/uwb/secure/iso7816/ResponseApduTest.java
new file mode 100644
index 0000000..96e3eb2
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/iso7816/ResponseApduTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.iso7816;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import org.junit.Test;
+
+/** Test cases for {@link ResponseApdu}. */
+public class ResponseApduTest {
+
+    @Test
+    public void generateResponseFromStatusWord() {
+        ResponseApdu response = ResponseApdu.fromStatusWord(StatusWord.SW_NO_ERROR);
+
+        assertThat(response.getStatusWord()).isEqualTo(0x9000);
+        assertThat(response.getResponseData().length).isEqualTo(0);
+    }
+
+    @Test
+    public  void generateResponseApdu() {
+        ResponseApdu responseApdu =
+                ResponseApdu.fromResponse(
+                        DataTypeConversionUtil.hexStringToByteArray("5A0201005C020100D401009000"));
+
+        assertThat(responseApdu.getStatusWord()).isEqualTo(0x9000);
+        assertThat(DataTypeConversionUtil.byteArrayToHexString(responseApdu.getResponseData()))
+                .isEqualTo("5A0201005C020100D40100");
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/iso7816/StatusWordTest.java b/service/tests/src/com/android/server/uwb/secure/iso7816/StatusWordTest.java
new file mode 100644
index 0000000..1170abb
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/iso7816/StatusWordTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.iso7816;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** Test cases for {@link StatusWord}. */
+public class StatusWordTest {
+
+    @Test
+    public void testFromInt_validStatusWord() {
+        StatusWord sw = StatusWord.fromInt(0x9000);
+        assertThat(sw).isEqualTo(StatusWord.SW_NO_ERROR);
+    }
+
+    @Test
+    public void testToBytes_noError() {
+        byte[] actual = StatusWord.SW_NO_ERROR.toBytes();
+        byte[] expected = {(byte) 0x90, (byte) 0x00};
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void testToBytes_unknown() {
+        byte[] actual = StatusWord.fromInt(0xDEAD).toBytes();
+        byte[] expected = {(byte) 0xDE, (byte) 0xAD};
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void testFromInt_tooManyBits() {
+        for (int sw : new int[] {-1, -70000, 70000, Integer.MAX_VALUE, Integer.MIN_VALUE}) {
+            boolean success = false;
+            try {
+                StatusWord.fromInt(sw);
+            } catch (IllegalArgumentException e) {
+                success = true;
+            } finally {
+                assertThat(success).isTrue();
+            }
+        }
+    }
+
+    @Test
+    public void testFromInt_goodNumberOfBits() {
+        for (int sw = 0; sw <= 0xfffe; sw += 100) {
+            StatusWord.fromInt(sw);
+        }
+        StatusWord.fromInt(0xffff);
+    }
+
+    @Test
+    public void testIsKnown() {
+        assertThat(StatusWord.fromInt(0x9000).isKnown()).isTrue();
+        assertThat(StatusWord.SW_NO_ERROR.isKnown()).isTrue();
+        assertThat(StatusWord.fromInt(0x1234).isKnown()).isFalse();
+    }
+
+    @Test
+    public void testCheckClassInvariant() throws IllegalAccessException {
+        // must not have public constructors.
+        assertThat(StatusWord.class.getConstructors()).isEmpty();
+
+        // all creators must be static and must not allow the caller to set a message.
+        int numCreators = 0;
+        for (Method method : StatusWord.class.getMethods()) {
+            if (method.getReturnType().isAssignableFrom(StatusWord.class)) {
+                numCreators++;
+                int modifiers = method.getModifiers();
+                assertThat(Modifier.isStatic(modifiers)).isTrue();
+                Class<?>[] params = method.getParameterTypes();
+                assertThat(params).hasLength(1);
+                assertThat(params[0]).isEqualTo(Integer.TYPE);
+            }
+        }
+        assertThat(numCreators).isEqualTo(1);
+
+        List<StatusWord> reflectivelyFoundKnownStatusWords = new ArrayList<>();
+        for (Field field : StatusWord.class.getFields()) {
+            int mod = field.getModifiers();
+            if (field.getType().equals(StatusWord.class)
+                    && Modifier.isPublic(mod)
+                    && Modifier.isStatic(mod)
+                    && Modifier.isFinal(mod)) {
+                reflectivelyFoundKnownStatusWords.add((StatusWord) field.get(null));
+            }
+        }
+
+        Set<StatusWord> expected = new HashSet<>(reflectivelyFoundKnownStatusWords);
+        assertThat(StatusWord.ALL_KNOWN_STATUS_WORDS).isEqualTo(expected);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/iso7816/TlvDatumTest.java b/service/tests/src/com/android/server/uwb/secure/iso7816/TlvDatumTest.java
new file mode 100644
index 0000000..b39b061
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/iso7816/TlvDatumTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.iso7816;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import  com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Test;
+
+/** Unit tests for {@link TlvDatum} */
+public class TlvDatumTest {
+
+    @Test
+    public void testTlvDatumToBytes() {
+        Tag childTag = new Tag((byte) 0x84);
+        Tag parentTag = new Tag((byte) 0x48);
+        TlvDatum tlvDatumChild =
+                new TlvDatum(
+                        childTag,
+                        DataTypeConversionUtil.hexStringToByteArray("A0000000041010"));
+
+        byte[] actualChild = tlvDatumChild.toBytes();
+        byte[] expectedChild = DataTypeConversionUtil.hexStringToByteArray("8407A0000000041010");
+
+        assertThat(actualChild).isEqualTo(expectedChild);
+
+        TlvDatum tlvDatumParent1 =
+                new TlvDatum(
+                        parentTag,
+                        tlvDatumChild);
+        byte[] actualParent1 = tlvDatumParent1.toBytes();
+        byte[] expectedParent = DataTypeConversionUtil.hexStringToByteArray(
+                "48098407A0000000041010");
+
+        assertThat(actualParent1).isEqualTo(expectedParent);
+
+        byte[] actualParent2 = new TlvDatum(
+                parentTag, ImmutableMap.of(childTag, ImmutableList.of(tlvDatumChild))).toBytes();
+
+        assertThat(actualParent2).isEqualTo(expectedParent);
+    }
+
+    @Test
+    public void testIntTlvDatum() {
+        int v = 0x01020304;
+        TlvDatum tlvDatum = new TlvDatum(new Tag((byte) 0x84), v);
+
+        byte[] actual = tlvDatum.toBytes();
+        byte[] expected = DataTypeConversionUtil.hexStringToByteArray("840401020304");
+
+        assertThat(actual).isEqualTo(expected);
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/iso7816/TlvParserTest.java b/service/tests/src/com/android/server/uwb/secure/iso7816/TlvParserTest.java
new file mode 100644
index 0000000..91cf91e
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/iso7816/TlvParserTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.iso7816;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
+import com.android.server.uwb.util.DataTypeConversionUtil;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/** Unit tests for {@link TlvParser} */
+public class TlvParserTest {
+
+    @Test
+    public void testParseComplicatedTlv() {
+        ResponseApdu responseApdu =
+                ResponseApdu.fromResponse(
+                        DataTypeConversionUtil.hexStringToByteArray(
+                                "6F1AA50F870101500A4D6173746572436172648407A00000000410109000"));
+        TlvDatum actual = TlvParser.parseTlvs(responseApdu).get(new Tag((byte) 0x6F)).get(0);
+
+        TlvDatum expected = new TlvDatum(new Tag((byte) 0x6F),
+                DataTypeConversionUtil.hexStringToByteArray(
+                        "A50F870101500A4D6173746572436172648407A0000000041010"));
+
+        assertThat(actual.toBytes()).isEqualTo(expected.toBytes());
+        assertTlvDatumEquals(expected, actual);
+    }
+
+    @Test
+    public void testParseSelectResponse() {
+        ResponseApdu responseApdu =
+                ResponseApdu.fromResponse(
+                        DataTypeConversionUtil.hexStringToByteArray("5A0201005C020100D401009000"));
+
+        TlvDatum subTlvDatum1 =
+                new TlvDatum(new Tag((byte) 0x5A),
+                        DataTypeConversionUtil.hexStringToByteArray("0100"));
+        TlvDatum subTlvDatum2 =
+                new TlvDatum(new Tag((byte) 0x5C),
+                        DataTypeConversionUtil.hexStringToByteArray("0100"));
+        TlvDatum subTlvDatum3 =
+                new TlvDatum(new Tag((byte) 0xD4),
+                        DataTypeConversionUtil.hexStringToByteArray("00"));
+        Map<Tag, List<TlvDatum>> result = TlvParser.parseTlvs(responseApdu);
+        List<TlvDatum> actual = Arrays.asList(
+                result.get(new Tag((byte) 0x5A)).get(0),
+                result.get(new Tag((byte) 0x5C)).get(0),
+                result.get(new Tag((byte) 0xD4)).get(0));
+        List<TlvDatum> expected = Arrays.asList(subTlvDatum1, subTlvDatum2, subTlvDatum3);
+
+        assertTlvDatumListEquals(expected, actual);
+    }
+
+    @Test
+    public void invalidInput_singleZero_failure() {
+        assertThat(TlvParser.parseTlvs(new byte[1])).isEmpty();
+    }
+
+    @Test
+    public void invalidInput_truncatedLength_failure() {
+        assertThat(TlvParser.parseTlvs(new byte[] {0x00, (byte) 0b10000001})).isEmpty();
+    }
+
+    @Test
+    public void noDataTag_success() {
+        Map<Tag, List<TlvDatum>> result = TlvParser.parseTlvs(new byte[] {0x5f, 0x5f, 0x00});
+        List<TlvDatum> actual = result.get(new Tag((byte) 0x5f, (byte) 0x5f));
+        assertThat(actual).isNotNull();
+        assertTlvDatumListEquals(
+                actual, ImmutableList.of(
+                        new TlvDatum(new Tag((byte) 0x5f, (byte) 0x5f), new byte[] {})));
+    }
+
+    private static void assertTlvDatumListEquals(List<TlvDatum> expected, List<TlvDatum> actual) {
+        assertThat(actual).hasSize(expected.size());
+        for (int i = 0; i < expected.size(); i++) {
+            assertTlvDatumEquals(expected.get(i), actual.get(i));
+        }
+    }
+
+    private static void assertTlvDatumEquals(TlvDatum expected, TlvDatum actual) {
+        assertTrue(equals(expected, actual));
+    }
+
+    /** Determine if two TlvDatums are equal. */
+    private static boolean equals(TlvDatum tlv1, TlvDatum tlv2) {
+        for (Map.Entry<Tag, List<TlvDatum>> tlv1SubEntry : tlv1.subTlvData.entrySet()) {
+            List<TlvDatum> tlv2SubList = tlv2.subTlvData.get(tlv1SubEntry.getKey());
+            assertTlvDatumListEquals(tlv1SubEntry.getValue(), tlv2SubList);
+            if (!Objects.equals(tlv1.tag, tlv2.tag) || !Arrays.equals(tlv1.value, tlv2.value)) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/secure/omapi/OmapiConnectionImplTest.java b/service/tests/src/com/android/server/uwb/secure/omapi/OmapiConnectionImplTest.java
new file mode 100644
index 0000000..3003ffa
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/secure/omapi/OmapiConnectionImplTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.secure.omapi;
+
+import static com.android.server.uwb.util.Constants.FIRA_APPLET_AID;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.se.omapi.Channel;
+import android.se.omapi.Reader;
+import android.se.omapi.SEService;
+import android.se.omapi.Session;
+
+import com.android.server.uwb.secure.iso7816.ResponseApdu;
+import com.android.server.uwb.secure.iso7816.StatusWord;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+
+import java.io.IOException;
+
+public class OmapiConnectionImplTest {
+    @Mock
+    Context mMockContext;
+    @Mock
+    SEService mMockSeService;
+    @Mock
+    Reader mMockReader;
+    @Mock
+    Session mMockSeSession;
+    @Mock
+    Channel mMockChannel;
+    @Rule
+    public ExpectedException mThrown = ExpectedException.none();
+
+    OmapiConnectionImpl mOmapiConnection;
+
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mOmapiConnection = new OmapiConnectionImpl(mMockContext);
+        mOmapiConnection.mSeService = mMockSeService;
+        when(mMockSeService.getReaders()).thenReturn(new Reader[]{mMockReader});
+        when(mMockSeService.isConnected()).thenReturn(true);
+        when(mMockReader.getName()).thenReturn("eSE");
+        when(mMockReader.openSession()).thenReturn(mMockSeSession);
+        when(mMockSeSession.openLogicalChannel(eq(FIRA_APPLET_AID))).thenReturn(mMockChannel);
+    }
+
+    @Test
+    public void openChannel() throws IOException {
+        when(mMockChannel.getSelectResponse())
+                .thenReturn(ResponseApdu.fromStatusWord(StatusWord.SW_NO_ERROR).toByteArray());
+        ResponseApdu selectResponse = mOmapiConnection.openChannel();
+
+        assertThat(selectResponse).isEqualTo(ResponseApdu.fromStatusWord(StatusWord.SW_NO_ERROR));
+    }
+
+    @Test
+    public void openChannelWithNullResponse() throws IOException {
+        mThrown.expect(IOException.class);
+
+        when(mMockChannel.getSelectResponse()).thenReturn(null);
+
+        mOmapiConnection.openChannel();
+    }
+
+    @Test
+    public void openChannel2Times() throws IOException {
+        when(mMockChannel.getSelectResponse())
+                .thenReturn(ResponseApdu.fromStatusWord(StatusWord.SW_NO_ERROR).toByteArray());
+
+        mOmapiConnection.openChannel();
+        ResponseApdu responseApdu = mOmapiConnection.openChannel();
+        assertThat(responseApdu.getStatusWord())
+                .isEqualTo(StatusWord.SW_NO_SPECIFIC_DIAGNOSTIC.toInt());
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/util/BinderUtil.java b/service/tests/src/com/android/server/uwb/util/BinderUtil.java
new file mode 100644
index 0000000..0dc3a36
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/util/BinderUtil.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.util;
+
+import android.os.Binder;
+
+/**
+ * Utilities for faking the calling uid in Binder.
+ */
+public class BinderUtil {
+    /**
+     * Fake the calling uid in Binder.
+     * @param uid the calling uid that Binder should return from now on
+     */
+    public static void setUid(int uid) {
+        Binder.restoreCallingIdentity((((long) uid) << 32) | Binder.getCallingPid());
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/util/DataTypeConversionUtilTest.java b/service/tests/src/com/android/server/uwb/util/DataTypeConversionUtilTest.java
new file mode 100644
index 0000000..8146d1d
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/util/DataTypeConversionUtilTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+public class DataTypeConversionUtilTest {
+    @Test
+    public void byteArrayToI32_success() {
+        assertThat(DataTypeConversionUtil.byteArrayToI32(new byte[]{0x01, 0x02, 0x03, 0x04}))
+                .isEqualTo(0x01020304);
+    }
+
+    @Test
+    public void byteArrayToI32_highByte_success() {
+        assertThat(
+                DataTypeConversionUtil.byteArrayToI32(
+                        new byte[]{(byte) 0xFF, (byte) 0xA5, (byte) 0xAA, (byte) 0xF0}))
+                .isEqualTo(0xFFA5AAF0);
+    }
+
+    @Test
+    public void byteArrayToI32_shortArray_Failure() {
+        assertThrows(
+                NumberFormatException.class,
+                () -> DataTypeConversionUtil.byteArrayToI32(new byte[]{0x01}));
+    }
+
+    @Test
+    public void byteArrayToI32_longArray_failure() {
+        assertThrows(
+                NumberFormatException.class,
+                () -> DataTypeConversionUtil.byteArrayToI32(
+                        new byte[]{0x01, 0x02, 0x03, 0x04, 0x05}));
+    }
+
+    @Test
+    public void i32ToByteArray_success() {
+        assertThat(DataTypeConversionUtil.i32ToByteArray(0x01020304))
+                .isEqualTo(new byte[] { (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04 });
+    }
+
+    @Test
+    public void i32ToLeByteArray_success() {
+        assertThat(DataTypeConversionUtil.i32ToLeByteArray(0x01020304))
+                .isEqualTo(new byte[] { (byte) 0x04, (byte) 0x03, (byte) 0x02, (byte) 0x01 });
+    }
+
+    @Test
+    public void byteArrayToHexString_byteString_success() {
+        String hexString = "010203040A0B0C0D";
+        assertThat(DataTypeConversionUtil.byteArrayToHexString(
+                new byte[] { (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,
+                        (byte) 0x0A, (byte) 0x0B, (byte) 0x0C, (byte) 0x0D }))
+                .isEqualTo(hexString);
+    }
+
+    @Test
+    public void byteArrayToHexString_null() {
+        assertThat(DataTypeConversionUtil.byteArrayToHexString(null)).isEmpty();
+    }
+
+    @Test
+    public void hexStringToByteArray_success() {
+        String hexString = "010203040A0B0C0D";
+        assertThat(DataTypeConversionUtil.hexStringToByteArray(hexString))
+                .isEqualTo(new byte[] { (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,
+                        (byte) 0x0A, (byte) 0x0B, (byte) 0x0C, (byte) 0x0D });
+    }
+
+    @Test
+    public void oneByteArbitraryByteArrayToI32() {
+        byte[] lengthBytes = {(byte) 178};
+        int actual = DataTypeConversionUtil.arbitraryByteArrayToI32(lengthBytes);
+
+        int expected = 178;
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void twoBytesArbitraryByteArrayToI32() {
+        byte[] lengthBytes = {(byte) 0x01, (byte) 0x1A};
+        int actual = DataTypeConversionUtil.arbitraryByteArrayToI32(lengthBytes);
+
+        int expected = 282;
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void threeBytesArbitraryByteArrayToI32() {
+        byte[] lengthBytes = {(byte) 0x01, (byte) 0x01, (byte) 0x1A};
+        int actual = DataTypeConversionUtil.arbitraryByteArrayToI32(lengthBytes);
+
+        int expected = 65818;
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void fourBytesArbitraryByteArrayToI32() {
+        byte[] lengthBytes = {(byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x1A};
+        int actual = DataTypeConversionUtil.arbitraryByteArrayToI32(lengthBytes);
+
+        int expected = 16843034;
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void fiveBytesArbitraryByteArrayToI32() {
+        assertThrows(
+                NumberFormatException.class,
+                () -> DataTypeConversionUtil.arbitraryByteArrayToI32(
+                        new byte[] {
+                                (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x1A, (byte) 0x01 }));
+
+        assertThrows(
+                NumberFormatException.class,
+                () -> DataTypeConversionUtil.arbitraryByteArrayToI32(
+                        new byte[0]));
+
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/util/HexTest.java b/service/tests/src/com/android/server/uwb/util/HexTest.java
new file mode 100644
index 0000000..5ad8b07
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/util/HexTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Test class for {@link Hex}.
+ */
+@RunWith(JUnit4.class)
+public class HexTest {
+
+    private final String mLower = "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff";
+    private final String mUpper = "F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF";
+    private final byte[] mBytes = {
+            (byte) 0xf0,
+            (byte) 0xf1,
+            (byte) 0xf2,
+            (byte) 0xf3,
+            (byte) 0xf4,
+            (byte) 0xf5,
+            (byte) 0xf6,
+            (byte) 0xf7,
+            (byte) 0xf8,
+            (byte) 0xf9,
+            (byte) 0xfa,
+            (byte) 0xfb,
+            (byte) 0xfc,
+            (byte) 0xfd,
+            (byte) 0xfe,
+            (byte) 0xff,
+    };
+
+    @Test
+    public void testClass() {
+        assertEquals(Hex.RADIX, 16);
+        assertEquals(Hex.RADIX, Hex.UPPER.length);
+        assertEquals(Hex.RADIX, Hex.LOWER.length);
+    }
+
+    @Test
+    public void testEncode() {
+        assertEquals(mLower, Hex.encode(mBytes));
+    }
+
+    @Test
+    public void testEncodeUpper() {
+        assertEquals(mUpper, Hex.encodeUpper(mBytes));
+    }
+
+    @Test
+    public void testDecodeLower() {
+        assertArrayEquals(mBytes, Hex.decode(mLower));
+    }
+
+    @Test
+    public void testDecodeUpper() {
+        assertArrayEquals(mBytes, Hex.decode(mUpper));
+    }
+
+    @Test
+    public void testDecodeEmptyString() {
+        assertArrayEquals(new byte[0], Hex.decode(""));
+    }
+
+    @Test
+    public void testDecodeOddLength() {
+        assertThat(Hex.decode("fff"))
+                .isEqualTo(DataTypeConversionUtil.hexStringToByteArray("0fff"));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testDecodeIllegalHighNibble() {
+        Hex.decode("g0");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testDecodeIllegalLowNibble() {
+        Hex.decode("0g");
+    }
+}
diff --git a/service/tests/src/com/android/server/uwb/util/ObjectIdentifierTest.java b/service/tests/src/com/android/server/uwb/util/ObjectIdentifierTest.java
new file mode 100644
index 0000000..51a93cc
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/util/ObjectIdentifierTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+package com.android.server.uwb.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class ObjectIdentifierTest {
+    @Test
+    public void equalValue() {
+        ObjectIdentifier oid1 = ObjectIdentifier.fromBytes(
+                DataTypeConversionUtil.hexStringToByteArray("010203"));
+        ObjectIdentifier oid2 = ObjectIdentifier.fromBytes(
+                DataTypeConversionUtil.hexStringToByteArray("010203"));
+
+        assertThat(oid1).isEqualTo(oid2);
+    }
+
+    @Test
+    public void notEqualValue() {
+        ObjectIdentifier oid1 = ObjectIdentifier.fromBytes(
+                DataTypeConversionUtil.hexStringToByteArray("010203"));
+        ObjectIdentifier oid2 = ObjectIdentifier.fromBytes(
+                DataTypeConversionUtil.hexStringToByteArray("010202"));
+
+        assertThat(oid1).isNotEqualTo(oid2);
+    }
+
+    @Test
+    public void sameInstance() {
+        ObjectIdentifier oid1 = ObjectIdentifier.fromBytes(
+                DataTypeConversionUtil.hexStringToByteArray("010203"));
+        ObjectIdentifier oid2 = oid1;
+
+        assertThat(oid1).isEqualTo(oid2);
+    }
+
+    @Test
+    public void differentClasses() {
+        ObjectIdentifier oid1 = ObjectIdentifier.fromBytes(
+                DataTypeConversionUtil.hexStringToByteArray("010203"));
+
+        assertThat(oid1).isNotEqualTo(0x0102);
+    }
+}
diff --git a/service/uci/Android.bp b/service/uci/Android.bp
new file mode 100755
index 0000000..c79bb1e
--- /dev/null
+++ b/service/uci/Android.bp
@@ -0,0 +1,7 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+subdirs = [

+    "*"

+]

diff --git a/service/uci/jni/Android.bp b/service/uci/jni/Android.bp
new file mode 100755
index 0000000..f6dbe92
--- /dev/null
+++ b/service/uci/jni/Android.bp
@@ -0,0 +1,72 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+    name: "libuwb_uci_jni_rust_defaults",
+    crate_name: "uwb_uci_jni_rust",
+    lints: "android",
+    clippy_lints: "android",
+    min_sdk_version: "Tiramisu",
+    srcs: ["rust/lib.rs"],
+    rustlibs: [
+        "libjni",
+        "libbinder_rs",
+        "liblog_rust",
+        "liblogger",
+        "libnum_traits",
+        "libuwb_uci_packets",
+        "libuwb_uci_rust",
+    ],
+    prefer_rlib: true,
+    apex_available: [
+        "com.android.uwb",
+    ],
+    host_supported: true,
+}
+
+rust_ffi_shared {
+    name: "libuwb_uci_jni_rust",
+    defaults: ["libuwb_uci_jni_rust_defaults"],
+}
+
+rust_test {
+    name: "libuwb_uci_jni_rust_tests",
+    defaults: ["libuwb_uci_jni_rust_defaults"],
+    target: {
+        android: {
+            test_suites: [
+                "general-tests",
+                "mts-uwb"
+            ],
+            test_config_template: "uwb_rust_test_config_template.xml",
+        },
+        host: {
+            test_suites: [
+                "general-tests",
+            ],
+            data_libs: [
+                "libandroid_runtime_lazy",
+                "libbase",
+                "libbinder",
+                "libbinder_ndk",
+                "libcutils",
+                "liblog",
+                "libutils",
+            ],
+        },
+    },
+    // Support multilib variants (using different suffix per sub-architecture), which is needed on
+    // build targets with secondary architectures, as the MTS test suite packaging logic flattens
+    // all test artifacts into a single `testcases` directory.
+    compile_multilib: "both",
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "",
+        },
+    },
+    auto_gen_config: true,
+}
diff --git a/service/uci/jni/UwbEventManager.cpp b/service/uci/jni/UwbEventManager.cpp
new file mode 100755
index 0000000..e670f7a
--- /dev/null
+++ b/service/uci/jni/UwbEventManager.cpp
@@ -0,0 +1,458 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Copyright 2021 NXP.
+ *
+ * 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 "UwbJniInternal.h"
+#include "UwbEventManager.h"
+#include "JniLog.h"
+#include "ScopedJniEnv.h"
+#include "SyncEvent.h"
+#include "UwbAdaptation.h"
+#include "uwb_config.h"
+#include "uwb_hal_int.h"
+
+namespace android {
+
+const char *RANGING_DATA_CLASS_NAME = "com/android/server/uwb/data/UwbRangingData";
+const char *RANGING_MEASURES_CLASS_NAME =
+    "com/android/server/uwb/data/UwbTwoWayMeasurement";
+/* ranging tdoa measures and multicast list update ntf events are implemented as
+   per Fira specification.
+       TODO support for these class to be added in service.*/
+const char *MULTICAST_UPDATE_LIST_DATA_CLASS_NAME =
+    "com/android/server/uwb/data/UwbMulticastListUpdateStatus";
+
+UwbEventManager UwbEventManager::mObjUwbManager;
+
+UwbEventManager &UwbEventManager::getInstance() { return mObjUwbManager; }
+
+UwbEventManager::UwbEventManager() {
+  mVm = NULL;
+  mClass = NULL;
+  mObject = NULL;
+  mRangeDataClass = NULL;
+  mRangingTwoWayMeasuresClass = NULL;
+  mRangeTdoaMeasuresClass = NULL;
+  mMulticastUpdateListDataClass = NULL;
+  mOnDeviceStateNotificationReceived = NULL;
+  mOnRangeDataNotificationReceived = NULL;
+  mOnSessionStatusNotificationReceived = NULL;
+  mOnCoreGenericErrorNotificationReceived = NULL;
+  mOnMulticastListUpdateNotificationReceived = NULL;
+  mOnBlinkDataTxNotificationReceived = NULL;
+  mOnRawUciNotificationReceived = NULL;
+  mOnVendorUciNotificationReceived = NULL;
+  mOnVendorDeviceInfo = NULL;
+}
+
+void UwbEventManager::onRangeDataNotificationReceived(
+    tUWA_RANGE_DATA_NTF *ranging_ntf_data) {
+  static const char fn[] = "onRangeDataNotificationReceived";
+  UNUSED(fn);
+
+  ScopedJniEnv env(mVm);
+  if (env == NULL) {
+    JNI_TRACE_E("%s: jni env is null", fn);
+    return;
+  }
+
+  jobject rangeDataObject;
+
+  if (ranging_ntf_data->ranging_measure_type == MEASUREMENT_TYPE_TWOWAY) {
+    JNI_TRACE_I("%s: ranging_measure_type = MEASUREMENT_TYPE_TWOWAY", fn);
+    jmethodID rngMeasuresCtor;
+    jmethodID rngDataCtorTwm;
+    jobjectArray rangeMeasuresArray;
+    rangeMeasuresArray =
+        env->NewObjectArray(ranging_ntf_data->no_of_measurements,
+                            mRangingTwoWayMeasuresClass, NULL);
+
+    /* Copy the data from structure to Java Object */
+    for (int i = 0; i < ranging_ntf_data->no_of_measurements; i++) {
+      jbyteArray macAddress;
+      jbyteArray rfu;
+
+      if (ranging_ntf_data->mac_addr_mode_indicator == SHORT_MAC_ADDRESS) {
+        macAddress = env->NewByteArray(2);
+        env->SetByteArrayRegion(
+            macAddress, 0, 2,
+            (jbyte *)ranging_ntf_data->ranging_measures.twr_range_measr[i]
+                .mac_addr);
+        rfu = env->NewByteArray(12);
+        env->SetByteArrayRegion(
+            rfu, 0, 12,
+            (jbyte *)ranging_ntf_data->ranging_measures.twr_range_measr[i].rfu);
+      } else {
+        macAddress = env->NewByteArray(8);
+        env->SetByteArrayRegion(
+            macAddress, 0, 8,
+            (jbyte *)ranging_ntf_data->ranging_measures.twr_range_measr[i]
+                .mac_addr);
+        rfu = env->NewByteArray(6);
+        env->SetByteArrayRegion(
+            rfu, 0, 6,
+            (jbyte *)ranging_ntf_data->ranging_measures.twr_range_measr[i].rfu);
+      }
+      rngMeasuresCtor = env->GetMethodID(mRangingTwoWayMeasuresClass, "<init>",
+                                         "([BIIIIIIIIIIII)V");
+
+      env->SetObjectArrayElement(
+          rangeMeasuresArray, i,
+          env->NewObject(
+              mRangingTwoWayMeasuresClass, rngMeasuresCtor, macAddress,
+              (int)ranging_ntf_data->ranging_measures.twr_range_measr[i].status,
+              (int)ranging_ntf_data->ranging_measures.twr_range_measr[i].nLos,
+              (int)ranging_ntf_data->ranging_measures.twr_range_measr[i]
+                  .distance,
+              (int)ranging_ntf_data->ranging_measures.twr_range_measr[i]
+                  .aoa_azimuth,
+              (int)ranging_ntf_data->ranging_measures.twr_range_measr[i]
+                  .aoa_azimuth_FOM,
+              (int)ranging_ntf_data->ranging_measures.twr_range_measr[i]
+                  .aoa_elevation,
+              (int)ranging_ntf_data->ranging_measures.twr_range_measr[i]
+                  .aoa_elevation_FOM,
+              (int)ranging_ntf_data->ranging_measures.twr_range_measr[i]
+                  .aoa_dest_azimuth,
+              (int)ranging_ntf_data->ranging_measures.twr_range_measr[i]
+                  .aoa_dest_azimuth_FOM,
+              (int)ranging_ntf_data->ranging_measures.twr_range_measr[i]
+                  .aoa_dest_elevation,
+              (int)ranging_ntf_data->ranging_measures.twr_range_measr[i]
+                  .aoa_dest_elevation_FOM,
+              (int)ranging_ntf_data->ranging_measures.twr_range_measr[i]
+                  .slot_index,
+              rfu));
+    }
+
+    rngDataCtorTwm = env->GetMethodID(
+        mRangeDataClass, "<init>",
+        "(JJIJIII[Lcom/android/server/uwb/data/UwbTwoWayMeasurement;)V");
+    rangeDataObject = env->NewObject(
+        mRangeDataClass, rngDataCtorTwm, (long)ranging_ntf_data->seq_counter,
+        (long)ranging_ntf_data->session_id,
+        (int)ranging_ntf_data->rcr_indication,
+        (long)ranging_ntf_data->curr_range_interval,
+        ranging_ntf_data->ranging_measure_type,
+        ranging_ntf_data->mac_addr_mode_indicator,
+        (int)ranging_ntf_data->no_of_measurements, rangeMeasuresArray);
+  }
+
+  if (mOnRangeDataNotificationReceived != NULL) {
+    env->CallVoidMethod(mObject, mOnRangeDataNotificationReceived,
+                        rangeDataObject);
+    if (env->ExceptionCheck()) {
+      env->ExceptionDescribe();
+      env->ExceptionClear();
+      JNI_TRACE_E("%s: fail to send range data", fn);
+    }
+  } else {
+    JNI_TRACE_E("%s: rangeDataNtf MID is NULL", fn);
+  }
+  JNI_TRACE_I("%s: exit", fn);
+}
+
+void UwbEventManager::onRawUciNotificationReceived(uint8_t *data,
+                                                   uint16_t length) {
+  JNI_TRACE_I("%s: Enter", __func__);
+
+  ScopedJniEnv env(mVm);
+  if (env == NULL) {
+    JNI_TRACE_E("%s: jni env is null", __func__);
+    return;
+  }
+
+  if (length == 0 || data == NULL) {
+    JNI_TRACE_E(
+        "%s: length is zero or data is NULL, skip sending notifications",
+        __func__);
+    return;
+  }
+
+  jbyteArray dataArray = env->NewByteArray(length);
+  env->SetByteArrayRegion(dataArray, 0, length, (jbyte *)data);
+
+  if (mOnRawUciNotificationReceived != NULL) {
+    env->CallVoidMethod(mObject, mOnRawUciNotificationReceived, dataArray);
+    if (env->ExceptionCheck()) {
+      env->ExceptionClear();
+      JNI_TRACE_E("%s: fail to send notification", __func__);
+    }
+  } else {
+    JNI_TRACE_E("%s: onRawUciNotificationReceived MID is NULL", __func__);
+  }
+  JNI_TRACE_I("%s: exit", __func__);
+}
+
+void UwbEventManager::onSessionStatusNotificationReceived(uint32_t sessionId,
+                                                          uint8_t state,
+                                                          uint8_t reasonCode) {
+  static const char fn[] = "notifySessionStateNotification";
+  UNUSED(fn);
+  JNI_TRACE_I("%s: enter; session ID=%x, State = %x reasonCode = %x", fn,
+              sessionId, state, reasonCode);
+
+  ScopedJniEnv env(mVm);
+  if (env == NULL) {
+    JNI_TRACE_E("%s: jni env is null", fn);
+    return;
+  }
+
+  if (mOnSessionStatusNotificationReceived != NULL) {
+    env->CallVoidMethod(mObject, mOnSessionStatusNotificationReceived,
+                        (long)sessionId, (int)state, (int)reasonCode);
+    if (env->ExceptionCheck()) {
+      env->ExceptionClear();
+      JNI_TRACE_E("%s: fail to notify", fn);
+    }
+  } else {
+    JNI_TRACE_E("%s: sessionStatusNtf MID is null ", fn);
+  }
+  JNI_TRACE_I("%s: exit", fn);
+}
+
+void UwbEventManager::onDeviceStateNotificationReceived(uint8_t state) {
+  static const char fn[] = "notifyDeviceStateNotification";
+  UNUSED(fn);
+  JNI_TRACE_I("%s: enter:  State = %x", fn, state);
+
+  ScopedJniEnv env(mVm);
+  if (env == NULL) {
+    JNI_TRACE_E("%s: jni env is null", fn);
+    return;
+  }
+
+  if (mOnDeviceStateNotificationReceived != NULL) {
+    env->CallVoidMethod(mObject, mOnDeviceStateNotificationReceived,
+                        (int)state);
+    if (env->ExceptionCheck()) {
+      env->ExceptionClear();
+      JNI_TRACE_E("%s: fail to notify", fn);
+    }
+  } else {
+    JNI_TRACE_E("%s: deviceStatusNtf MID is null ", fn);
+  }
+  JNI_TRACE_I("%s: exit", fn);
+}
+
+void UwbEventManager::onCoreGenericErrorNotificationReceived(uint8_t state) {
+  static const char fn[] = "notifyCoreGenericErrorNotification";
+  UNUSED(fn);
+  JNI_TRACE_I("%s: enter:  State = %x", fn, state);
+
+  ScopedJniEnv env(mVm);
+  if (env == NULL) {
+    JNI_TRACE_E("%s: jni env is null", fn);
+    return;
+  }
+
+  if (mOnCoreGenericErrorNotificationReceived != NULL) {
+    env->CallVoidMethod(mObject, mOnCoreGenericErrorNotificationReceived,
+                        (int)state);
+    if (env->ExceptionCheck()) {
+      env->ExceptionClear();
+      JNI_TRACE_E("%s: fail to notify", fn);
+    }
+  } else {
+    JNI_TRACE_E("%s: genericErrorStatusNtf MID is null ", fn);
+  }
+
+  JNI_TRACE_I("%s: exit", fn);
+}
+
+void UwbEventManager::onMulticastListUpdateNotificationReceived(
+    tUWA_SESSION_UPDATE_MULTICAST_LIST_NTF *multicast_list_ntf) {
+  static const char fn[] = "onMulticastListUpdateNotificationReceived";
+  UNUSED(fn);
+  JNI_TRACE_I("%s: enter;", fn);
+
+  ScopedJniEnv env(mVm);
+  if (env == NULL) {
+    JNI_TRACE_E("%s: jni env is null", fn);
+    return;
+  }
+
+  if (multicast_list_ntf == NULL) {
+    JNI_TRACE_E("%s: multicast_list_ntf is null", fn);
+    return;
+  }
+
+  jintArray controleeMacAddressArray =
+      env->NewIntArray(multicast_list_ntf->no_of_controlees);
+  jlongArray subSessionIdArray =
+      env->NewLongArray(multicast_list_ntf->no_of_controlees);
+  jintArray statusArray =
+      env->NewIntArray(multicast_list_ntf->no_of_controlees);
+
+  if (multicast_list_ntf->no_of_controlees > 0) {
+    uint32_t controleeMacAddressList[multicast_list_ntf->no_of_controlees];
+    uint32_t statusList[multicast_list_ntf->no_of_controlees];
+    uint64_t subSessionIdList[multicast_list_ntf->no_of_controlees];
+    for (int i = 0; i < multicast_list_ntf->no_of_controlees; i++) {
+      controleeMacAddressList[i] =
+          multicast_list_ntf->controlee_mac_address_list[i];
+      statusList[i] = multicast_list_ntf->status_list[i];
+    }
+    for (int i = 0; i < multicast_list_ntf->no_of_controlees; i++) {
+      subSessionIdList[i] = multicast_list_ntf->subsession_id_list[i];
+    }
+    env->SetIntArrayRegion(controleeMacAddressArray, 0,
+                           multicast_list_ntf->no_of_controlees,
+                           (jint *)controleeMacAddressList);
+    env->SetLongArrayRegion(subSessionIdArray, 0,
+                            multicast_list_ntf->no_of_controlees,
+                            (jlong *)subSessionIdList);
+    env->SetIntArrayRegion(statusArray, 0, multicast_list_ntf->no_of_controlees,
+                           (jint *)statusList);
+  }
+  jmethodID multicastUpdateListDataCtor =
+      env->GetMethodID(mMulticastUpdateListDataClass, "<init>", "(JII[I[J[I)V");
+  jobject multicastUpdateListDataObject =
+      env->NewObject(mMulticastUpdateListDataClass, multicastUpdateListDataCtor,
+                     (long)multicast_list_ntf->session_id,
+                     (int)multicast_list_ntf->remaining_list,
+                     (int)multicast_list_ntf->no_of_controlees,
+                     controleeMacAddressArray, subSessionIdArray, statusArray);
+
+  if (mOnMulticastListUpdateNotificationReceived != NULL) {
+    env->CallVoidMethod(mObject, mOnMulticastListUpdateNotificationReceived,
+                        multicastUpdateListDataObject);
+    if (env->ExceptionCheck()) {
+      env->ExceptionClear();
+      JNI_TRACE_E("%s: fail to send Multicast update list ntf", fn);
+    }
+  } else {
+    JNI_TRACE_E("%s: MulticastUpdateListNtf MID is null ", fn);
+  }
+  JNI_TRACE_I("%s: exit", fn);
+}
+
+void UwbEventManager::onBlinkDataTxNotificationReceived(uint8_t status) {
+  static const char fn[] = "onBlinkDataTxNotificationReceived";
+  UNUSED(fn);
+  JNI_TRACE_I("%s: enter:  State = %x", fn, status);
+
+  ScopedJniEnv env(mVm);
+  if (env == NULL) {
+    JNI_TRACE_E("%s: jni env is null", fn);
+    return;
+  }
+
+  if (mOnBlinkDataTxNotificationReceived != NULL) {
+    env->CallVoidMethod(mObject, mOnBlinkDataTxNotificationReceived,
+                        (int)status);
+    if (env->ExceptionCheck()) {
+      env->ExceptionClear();
+      JNI_TRACE_E("%s: fail to notify", fn);
+    }
+  } else {
+    JNI_TRACE_E("%s: BlikDataTxNtf MID is null ", fn);
+  }
+  JNI_TRACE_I("%s: exit", fn);
+}
+
+void UwbEventManager::onVendorUciNotificationReceived(uint8_t gid, uint8_t oid, uint8_t* data, uint16_t length) {
+  static const char fn[] = "onVendorUciNotificationReceived";
+  UNUSED(fn);
+
+  ScopedJniEnv env(mVm);
+  if (env == NULL) {
+    JNI_TRACE_E("%s: jni env is null", fn);
+    return;
+  }
+
+  jbyteArray dataArray = env->NewByteArray(length);
+  env->SetByteArrayRegion(dataArray, 0, length, (jbyte*)data);
+
+  if (mOnVendorUciNotificationReceived != NULL) {
+    env->CallVoidMethod(mObject, mOnVendorUciNotificationReceived, (int)gid, (int)oid, dataArray);
+    if (env->ExceptionCheck()) {
+      env->ExceptionClear();
+      JNI_TRACE_E("%s: fail to send notification", __func__);
+    }
+  } else {
+    JNI_TRACE_E("%s: onVendorUciNotificationReceived MID is NULL", __func__);
+  }
+  JNI_TRACE_I("%s: exit", __func__);
+}
+
+void UwbEventManager::onVendorDeviceInfo(uint8_t* data, uint8_t length) {
+  static const char fn[] = "onVendorDeviceInfo";
+  UNUSED(fn);
+  if((length <= 0) || (data == NULL)) {
+        JNI_TRACE_E("%s: data len is Zero or vendorDevice info  is NULL", fn);
+        return;
+  }
+
+  ScopedJniEnv env(mVm);
+
+  jbyteArray dataArray = env->NewByteArray(length);
+  env->SetByteArrayRegion(dataArray, 0, length, (jbyte*)data);
+  if (mOnVendorDeviceInfo != NULL) {
+    env->CallVoidMethod(mObject, mOnVendorDeviceInfo, dataArray);
+    if (env->ExceptionCheck()) {
+      env->ExceptionClear();
+      JNI_TRACE_E("%s: fail to vendor info", __func__);
+    }
+  } else {
+    JNI_TRACE_E("%s: onVendorDeviceInfo MID is NULL", __func__);
+  }
+  JNI_TRACE_I("%s: exit", __func__);
+}
+
+void UwbEventManager::doLoadSymbols(JNIEnv *env, jobject thiz) {
+  static const char fn[] = "UwbEventManager::doLoadSymbols";
+  UNUSED(fn);
+  JNI_TRACE_I("%s: enter", fn);
+  env->GetJavaVM(&mVm);
+
+  jclass clazz = env->GetObjectClass(thiz);
+  if (clazz != NULL) {
+    mClass = (jclass)env->NewGlobalRef(clazz);
+    // The reference is only used as a proxy for callbacks.
+    mObject = env->NewGlobalRef(thiz);
+
+    mOnDeviceStateNotificationReceived =
+        env->GetMethodID(clazz, "onDeviceStatusNotificationReceived", "(I)V");
+    mOnRangeDataNotificationReceived =
+        env->GetMethodID(clazz, "onRangeDataNotificationReceived",
+                         "(Lcom/android/server/uwb/data/UwbRangingData;)V");
+    mOnSessionStatusNotificationReceived = env->GetMethodID(
+        clazz, "onSessionStatusNotificationReceived", "(JII)V");
+    mOnCoreGenericErrorNotificationReceived = env->GetMethodID(
+        clazz, "onCoreGenericErrorNotificationReceived", "(I)V");
+
+    // TDB, this should be reworked
+    mOnMulticastListUpdateNotificationReceived = env->GetMethodID(
+        clazz, "onMulticastListUpdateNotificationReceived",
+        "(Lcom/android/server/uwb/data/UwbMulticastListUpdateStatus;)V");
+    mOnRawUciNotificationReceived = env->GetMethodID(clazz,
+            "onRawUciNotificationReceived", "([B)V");
+    mOnVendorUciNotificationReceived = env->GetMethodID(clazz,
+            "onVendorUciNotificationReceived", "(II[B)V");
+    mOnVendorDeviceInfo = env->GetMethodID(clazz,
+            "onVendorDeviceInfo", "([B)V");
+
+    uwb_jni_cache_jclass(env, RANGING_DATA_CLASS_NAME, &mRangeDataClass);
+    uwb_jni_cache_jclass(env, RANGING_MEASURES_CLASS_NAME,
+                         &mRangingTwoWayMeasuresClass);
+    uwb_jni_cache_jclass(env, MULTICAST_UPDATE_LIST_DATA_CLASS_NAME,
+                         &mMulticastUpdateListDataClass);
+  }
+  JNI_TRACE_I("%s: exit", fn);
+}
+} // namespace android
diff --git a/service/uci/jni/UwbEventManager.h b/service/uci/jni/UwbEventManager.h
new file mode 100755
index 0000000..f95e92e
--- /dev/null
+++ b/service/uci/jni/UwbEventManager.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Copyright 2021 NXP.
+ *
+ * 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 _UWB_NATIVE_MANAGER_H_
+#define _UWB_NATIVE_MANAGER_H_
+
+namespace android {
+
+class UwbEventManager {
+public:
+  static UwbEventManager &getInstance();
+  void doLoadSymbols(JNIEnv *env, jobject o);
+
+  void onDeviceStateNotificationReceived(uint8_t state);
+  void onRangeDataNotificationReceived(tUWA_RANGE_DATA_NTF *ranging_ntf_data);
+  void onRawUciNotificationReceived(uint8_t *data, uint16_t length);
+  void onSessionStatusNotificationReceived(uint32_t sessionId, uint8_t state,
+                                           uint8_t reasonCode);
+  void onCoreGenericErrorNotificationReceived(uint8_t state);
+  void onMulticastListUpdateNotificationReceived(
+      tUWA_SESSION_UPDATE_MULTICAST_LIST_NTF *multicast_list_ntf);
+  void onBlinkDataTxNotificationReceived(uint8_t state);
+  void onVendorUciNotificationReceived(uint8_t gid, uint8_t oid, uint8_t* data, uint16_t length);
+  void onVendorDeviceInfo(uint8_t* data, uint8_t length);
+
+private:
+  UwbEventManager();
+
+  static UwbEventManager mObjUwbManager;
+
+  JavaVM *mVm;
+
+  jclass mClass;   // Reference to Java  class
+  jobject mObject; // Weak ref to Java object to call on
+
+  jclass mRangeDataClass;
+  jclass mRangingTwoWayMeasuresClass;
+  jclass mRangeTdoaMeasuresClass;
+  jclass mMulticastUpdateListDataClass;
+
+  jmethodID mOnRangeDataNotificationReceived;
+  jmethodID mOnSessionStatusNotificationReceived;
+  jmethodID mOnCoreGenericErrorNotificationReceived;
+  jmethodID mOnMulticastListUpdateNotificationReceived;
+  // TODO following native methods to be implemented in native layer.
+  jmethodID mOnDeviceStateNotificationReceived;
+  jmethodID mOnBlinkDataTxNotificationReceived;
+  jmethodID mOnRawUciNotificationReceived;
+  jmethodID mOnVendorUciNotificationReceived;
+  jmethodID mOnVendorDeviceInfo;
+};
+
+} // namespace android
+#endif
diff --git a/service/uci/jni/UwbJniInternal.h b/service/uci/jni/UwbJniInternal.h
new file mode 100755
index 0000000..d031e27
--- /dev/null
+++ b/service/uci/jni/UwbJniInternal.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Copyright 2021 NXP.
+ *
+ * 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 _UWBAPI_INTERNAL_H_
+#define _UWBAPI_INTERNAL_H_
+
+#include <nativehelper/ScopedLocalRef.h>
+
+#include "UwbJniTypes.h"
+#include "UwbJniUtil.h"
+#include "uwa_api.h"
+
+namespace android {
+
+#define UWB_CMD_TIMEOUT 4000 // JNI API wait timout
+
+/* extern declarations */
+extern bool uwb_debug_enabled;
+extern bool gIsUwaEnabled;
+
+void clearRfTestContext();
+void uwaRfTestDeviceManagementCallback(uint8_t dmEvent,
+                                       tUWA_DM_TEST_CBACK_DATA *eventData);
+} // namespace android
+#endif
diff --git a/service/uci/jni/UwbJniTypes.h b/service/uci/jni/UwbJniTypes.h
new file mode 100755
index 0000000..d3316a2
--- /dev/null
+++ b/service/uci/jni/UwbJniTypes.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Copyright 2021 NXP.
+ *
+ * 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 _UWB_JNI_TYPES_
+#define _UWB_JNI_TYPES_
+
+#include <array>
+#include <deque>
+#include <map>
+#include <mutex>
+#include <numeric>
+
+#include "SyncEvent.h"
+#include "uci_defs.h"
+#include "uwa_api.h"
+
+typedef struct UwbDeviceInfo {
+  uint16_t uciVersion;
+  uint16_t macVersion;
+  uint16_t phyVersion;
+  uint16_t uciTestVersion;
+} deviceInfo_t;
+
+typedef struct conformanceTestData {
+  SyncEvent ConfigEvt;
+  tUWA_STATUS wstatus;
+  uint8_t rsp_data[CONFORMANCE_TEST_MAX_UCI_PKT_LENGTH];
+  uint8_t rsp_len;
+} conformanceTestData_t;
+
+/* Session Data contains M distance samples of N Anchors in order to provide
+ * averaged distance for every anchor */
+/* N is Maximum Number of Anchors(MAX_NUM_RESPONDERS) */
+/* Where M is sampling Rate, the Max value is defined by Service */
+typedef struct sessionRangingData {
+  uint8_t samplingRate;
+  std::array<std::deque<uint32_t>, MAX_NUM_RESPONDERS> anchors;
+} SessionRangingData;
+
+#endif
diff --git a/service/uci/jni/UwbNativeManager.cpp b/service/uci/jni/UwbNativeManager.cpp
new file mode 100755
index 0000000..2373431
--- /dev/null
+++ b/service/uci/jni/UwbNativeManager.cpp
@@ -0,0 +1,1845 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Copyright 2021 NXP.
+ *
+ * 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 <vector>
+
+#include "UwbJniInternal.h"
+#include "JniLog.h"
+#include "ScopedJniEnv.h"
+#include "SyncEvent.h"
+#include "UwbAdaptation.h"
+#include "UwbEventManager.h"
+#include "uwb_api.h"
+#include "uwb_config.h"
+#include "uwb_hal_int.h"
+
+#define INVALID_SESSION_ID 0xFFFFFFFF
+
+namespace android {
+
+const char *UWB_NATIVE_MANAGER_CLASS_NAME =
+    "com/android/server/uwb/jni/NativeUwbManager";
+
+bool uwb_debug_enabled = true;
+static conformanceTestData_t ConformanceDataConf;
+
+static std::map<unsigned int, SessionRangingData> sAveragedRangingData;
+static std::mutex sSessionMutex;
+
+bool gIsUwaEnabled = false;
+bool gIsMaxPpmValueAvailable = false;
+
+static SyncEvent sUwaEnableEvent;        // event for UWA_Enable()
+static SyncEvent sUwaDisableEvent;       // event for UWA_Disable
+static SyncEvent sUwaSetConfigEvent;     // event for Set_Config....
+static SyncEvent sUwaSetAppConfigEvent;  // event for Set_AppConfig....
+static SyncEvent sUwaGetConfigEvent;     // event for Get_Config....
+static SyncEvent sUwaGetAppConfigEvent;  // event for Get_AppConfig....
+static SyncEvent sUwaDeviceResetEvent;   // event for deviceResetEvent
+static SyncEvent sUwaRngStartEvent;      // event for ranging start
+static SyncEvent sUwaRngStopEvent;       // event for ranging stop
+static SyncEvent sUwadeviceNtfEvent;     // event for device status NTF
+static SyncEvent sUwaSessionInitEvent;   // event for sessionInit resp
+static SyncEvent sUwaSessionDeInitEvent; // event for sessionDeInit resp
+static SyncEvent
+    sUwaGetSessionCountEvent;            // event for get session count response
+static SyncEvent sUwaGetDeviceInfoEvent; // event for get Device Info
+static SyncEvent
+    sUwaGetRangingCountEvent; // event for get ranging count response
+static SyncEvent sUwaGetSessionStatusEvent; // event for get the session status
+static SyncEvent
+    sUwaMulticastListUpdateEvent; // event for
+                                  // UWA_ControllerMulticastListUpdate
+static SyncEvent sUwaSendBlinkDataEvent;
+static SyncEvent sErrNotify;
+static SyncEvent sUwaSetCountryCodeEvent; // event for
+                                          // UWA_ControllerSetCountryCode
+static SyncEvent sUwaSendRawUciEvt; // event for UWA_SendRawCommand
+static SyncEvent sUwaGetDeviceCapsEvent; // event for Get Device Capabilities
+
+static deviceInfo_t sUwbDeviceInfo;
+static uint8_t sSetAppConfig[UCI_MAX_PAYLOAD_SIZE];
+static uint8_t sGetAppConfig[UCI_MAX_PAYLOAD_SIZE];
+static uint8_t sGetCoreConfig[UCI_MAX_PAYLOAD_SIZE];
+static uint8_t sSetCoreConfig[UCI_MAX_PAYLOAD_SIZE];
+static uint8_t sUwbDeviceCapability[UCI_MAX_PKT_SIZE];
+static uint8_t sSendRawResData[UCI_MAX_PAYLOAD_SIZE];
+static uint32_t sRangingCount = 0;
+static uint8_t sNoOfAppConfigIds = 0x00;
+static uint8_t sNoOfCoreConfigIds = 0x00;
+static uint8_t sSessionCount = -1;
+static uint16_t sDevCapInfoLen = 0;
+static uint16_t sDevCapInfoIds = 0x00;
+static uint16_t sGetCoreConfigLen;
+static uint16_t sGetAppConfigLen;
+static uint16_t sSetAppConfigLen;
+static uint8_t sGetAppConfigStatus;
+static uint8_t sSetAppConfigStatus;
+static uint8_t sSendBlinkDataStatus;
+static uint16_t sSendRawResLen;
+
+/* command response status */
+static bool sSessionInitStatus = false;
+static bool sSessionDeInitStatus = false;
+static bool sIsDeviceResetDone =
+    false; // whether Reset Performed is Successful is done or not
+static bool sRangeStartStatus = false;
+static bool sRangeStopStatus = false;
+static bool sSetAppConfigRespStatus = false;
+static bool sGetAppConfigRespStatus = false;
+static bool sMulticastListUpdateStatus = false;
+static bool sSetCountryCodeStatus = false;
+static bool sGetDeviceCapsRespStatus = false;
+
+static uint8_t sSessionState = UWB_UNKNOWN_SESSION;
+
+static eUWBS_DEVICE_STATUS_t sDeviceState = UWBS_STATUS_ERROR;
+
+static UwbEventManager &uwbEventManager = UwbEventManager::getInstance();
+
+jint MSB_BITMASK = 0x000000FF;
+
+/* Function to calculate and update ranging data averaging value into ranging
+ * data notification */
+static void update_ranging_data_average(tUWA_RANGE_DATA_NTF *rangingDataNtf) {
+  static const char fn[] = "update_ranging_data_average";
+  UNUSED(fn);
+  // Get Current Session Data
+  SessionRangingData &sessionData =
+      sAveragedRangingData[rangingDataNtf->session_id];
+
+  // Calculate the Average of N Distances for every Anchor, Where N is Sampling
+  // Rate for that Anchor
+  for (int i = 0; i < rangingDataNtf->no_of_measurements; i++) {
+    // Get current Two way measures object
+    tUWA_TWR_RANGING_MEASR &twr_range_measr =
+        rangingDataNtf->ranging_measures.twr_range_measr[i];
+    // Get the Current Anchor Distance Queue
+    auto &anchorDistanceQueue = sessionData.anchors[i];
+    JNI_TRACE_I("%s: Input Distance is: %d", fn, twr_range_measr.distance);
+    // If Number of distances in Queue is more than Sampling Rate
+    if (anchorDistanceQueue.size() >= sessionData.samplingRate) {
+      // Remove items from the queue until items in the queue is one less than
+      // sampling rate
+      while (anchorDistanceQueue.size() >= sessionData.samplingRate) {
+        JNI_TRACE_I("%s: Distance Popped from Queue: %d", fn,
+                    anchorDistanceQueue.front());
+        anchorDistanceQueue.pop_front();
+      }
+    }
+    // Push the New distance item into the Anchor Distance Queue
+    anchorDistanceQueue.push_back(twr_range_measr.distance);
+    // Calculate average of items(Except where distance is FFFF)
+    // in the Queue and update averaged distance into the distance field
+    uint32_t divider = 0;
+    uint32_t sum = 0;
+    for (auto it = anchorDistanceQueue.begin(); it != anchorDistanceQueue.end();
+         ++it) {
+      if (*it != 0xFFFF) {
+        sum = (uint32_t)(sum + *it);
+        divider++;
+      }
+    }
+    if (divider > 0) {
+      twr_range_measr.distance = sum / divider;
+    } else {
+      twr_range_measr.distance = 0xFFFF;
+    }
+    JNI_TRACE_I("%s: Averaged Distance is: %d", fn, twr_range_measr.distance);
+  }
+}
+
+/*******************************************************************************
+**
+** Function:        notifyRangeDataNotification
+**
+** Description:     Notify the Range data  to application
+**
+** Returns:         void
+**
+*******************************************************************************/
+void notifyRangeDataNotification(tUWA_RANGE_DATA_NTF *ranging_data) {
+  static const char fn[] = "notifyRangeDataNotification";
+  UNUSED(fn);
+  JNI_TRACE_I("%s: Enter", fn);
+
+  if (ranging_data->ranging_measure_type == ONE_WAY_RANGING) {
+    uwbEventManager.onRangeDataNotificationReceived(ranging_data);
+  } else {
+    {
+      std::unique_lock<std::mutex> lock(sSessionMutex);
+      unsigned int session_id = ranging_data->session_id;
+      auto it = sAveragedRangingData.find(session_id);
+      if (it != sAveragedRangingData.end()) {
+        if (sAveragedRangingData[session_id].samplingRate > 1) {
+          JNI_TRACE_I("%s: Before Averaging", fn);
+          update_ranging_data_average(ranging_data);
+          JNI_TRACE_I("%s: After Averaging", fn);
+        }
+      }
+    }
+    uwbEventManager.onRangeDataNotificationReceived(ranging_data);
+  }
+}
+
+/*******************************************************************************
+**
+** Function:        uwaDeviceManagementCallback
+**
+** Description:     Receive device management events from UCI stack.
+**                  dmEvent: Device-management event ID.
+**                  eventData: Data associated with event ID.
+**
+** Returns:         None
+**
+*******************************************************************************/
+static void uwaDeviceManagementCallback(uint8_t dmEvent,
+                                        tUWA_DM_CBACK_DATA *eventData) {
+  static const char fn[] = "uwaDeviceManagementCallback";
+  UNUSED(fn);
+  JNI_TRACE_I("%s: enter; event=0x%X", fn, dmEvent);
+
+  switch (dmEvent) {
+  case UWA_DM_ENABLE_EVT: /* Result of UWA_Enable */
+  {
+    SyncEventGuard guard(sUwaEnableEvent);
+    JNI_TRACE_I("%s: uwa_dm_enable_EVT; status=0x%X", fn, eventData->status);
+    gIsUwaEnabled = eventData->status == UWA_STATUS_OK;
+    sUwaEnableEvent.notifyOne();
+  } break;
+
+  case UWA_DM_DISABLE_EVT: /* Result of UWA_Disable */
+  {
+    SyncEventGuard guard(sUwaDisableEvent);
+    JNI_TRACE_I("%s: UWA_DM_DISABLE_EVT", fn);
+    gIsUwaEnabled = false;
+    sUwaDisableEvent.notifyOne();
+  } break;
+  case UWA_DM_DEVICE_RESET_RSP_EVT: // result of UWA_SendDeviceReset
+  {
+    JNI_TRACE_I("%s: UWA_DM_DEVICE_RESET_RSP_EVT", fn);
+    SyncEventGuard guard(sUwaDeviceResetEvent);
+    if (eventData->status != UWA_STATUS_OK) {
+      JNI_TRACE_E("%s: UWA_DM_DEVICE_RESET_RSP_EVT failed", fn);
+    } else {
+      sIsDeviceResetDone = true;
+    }
+    sUwaDeviceResetEvent.notifyOne();
+  } break;
+  case UWA_DM_DEVICE_STATUS_NTF_EVT:
+    JNI_TRACE_I("%s: UWA_DM_DEVICE_STATUS_NTF_EVT", fn);
+    {
+      JNI_TRACE_I("device status = %x", eventData->dev_status.status);
+      SyncEventGuard guard(sUwadeviceNtfEvent);
+      sDeviceState = (eUWBS_DEVICE_STATUS_t)eventData->dev_status.status;
+      if (sDeviceState == UWBS_STATUS_ERROR)
+        sErrNotify.notifyAll();
+      else
+        sUwadeviceNtfEvent.notifyOne();
+      uwbEventManager.onDeviceStateNotificationReceived(sDeviceState);
+    }
+    break;
+  case UWA_DM_CORE_GET_DEVICE_INFO_RSP_EVT:
+    JNI_TRACE_I("%s: UWA_DM_CORE_GET_DEVICE_INFO_RSP_EVT", fn);
+    {
+      SyncEventGuard guard(sUwaGetDeviceInfoEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        sUwbDeviceInfo.uciVersion = eventData->sGet_device_info.uci_version;
+        sUwbDeviceInfo.macVersion = eventData->sGet_device_info.mac_version;
+        sUwbDeviceInfo.phyVersion = eventData->sGet_device_info.phy_version;
+        sUwbDeviceInfo.uciTestVersion =
+            eventData->sGet_device_info.uciTest_version;
+        uwbEventManager.onVendorDeviceInfo(eventData->sGet_device_info.vendor_info, eventData->sGet_device_info.vendor_info_len);
+      } else {
+        JNI_TRACE_E("%s: UWA_DM_CORE_GET_DEVICE_INFO_RSP_EVT failed", fn);
+      }
+      sUwaGetDeviceInfoEvent.notifyOne();
+    }
+    break;
+  case UWA_DM_CORE_SET_CONFIG_RSP_EVT: // result of UWA_SetCoreConfig
+    JNI_TRACE_I("%s: UWA_DM_CORE_SET_CONFIG_RSP_EVT", fn);
+    {
+      if (eventData->status != UWA_STATUS_OK) {
+        JNI_TRACE_E("%s: UWA_DM_CORE_SET_CONFIG_RSP_EVT failed", fn);
+      }
+      if (eventData->sCore_set_config.tlv_size > 0) {
+        memcpy(sSetCoreConfig, eventData->sCore_set_config.param_ids,
+               eventData->sCore_set_config.tlv_size);
+      }
+      SyncEventGuard guard(sUwaSetConfigEvent);
+      sUwaSetConfigEvent.notifyOne();
+    }
+    break;
+  case UWA_DM_CORE_GET_CONFIG_RSP_EVT: /* Result of UWA_GetCoreConfig */
+    JNI_TRACE_I("%s: UWA_DM_CORE_GET_CONFIG_RSP_EVT", fn);
+    {
+      SyncEventGuard guard(sUwaGetConfigEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        sGetCoreConfigLen = eventData->sCore_get_config.tlv_size;
+        sNoOfCoreConfigIds = eventData->sCore_get_config.no_of_ids;
+      } else {
+        JNI_TRACE_E("%s: UWA_DM_GET_CONFIG failed", fn);
+        /* As of now will cary the failed ids list till this point */
+        sGetCoreConfigLen = 0;
+        sNoOfCoreConfigIds = 0;
+      }
+      if (eventData->sCore_get_config.tlv_size > 0 &&
+          eventData->sCore_get_config.tlv_size <= sizeof(sGetCoreConfig)) {
+        memcpy(sGetCoreConfig, eventData->sCore_get_config.param_tlvs,
+               eventData->sCore_get_config.tlv_size);
+      }
+      sUwaGetConfigEvent.notifyOne();
+    }
+    break;
+  case UWA_DM_SESSION_INIT_RSP_EVT:
+    JNI_TRACE_I("%s: UWA_DM_SESSION_INIT_RSP_EVT", fn);
+    {
+      SyncEventGuard guard(sUwaSessionInitEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        sSessionInitStatus = true;
+        JNI_TRACE_I("%s: UWA_DM_SESSION_INIT_RSP_EVT Success", fn);
+      } else {
+        JNI_TRACE_E("%s: UWA_DM_SESSION_INIT_RSP_EVT failed", fn);
+      }
+      sUwaSessionInitEvent.notifyOne();
+    }
+    break;
+  case UWA_DM_SESSION_DEINIT_RSP_EVT:
+    JNI_TRACE_I("%s: UWA_DM_SESSION_DEINIT_RSP_EVT", fn);
+    {
+      SyncEventGuard guard(sUwaSessionDeInitEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        sSessionDeInitStatus = true;
+        JNI_TRACE_I("%s: UWA_DM_SESSION_DEINIT_RSP_EVT Success", fn);
+      } else {
+        JNI_TRACE_E("%s: UWA_DM_SESSION_DEINIT_RSP_EVT failed", fn);
+      }
+      sUwaSessionDeInitEvent.notifyOne();
+    }
+    break;
+  case UWA_DM_SESSION_STATUS_NTF_EVT:
+    JNI_TRACE_I("%s: UWA_DM_SESSION_STATUS_NTF_EVT", fn);
+    {
+      unsigned int session_id = eventData->sSessionStatus.session_id;
+
+      if (UWB_SESSION_DEINITIALIZED == eventData->sSessionStatus.state) {
+        std::unique_lock<std::mutex> lock(sSessionMutex);
+        auto it = sAveragedRangingData.find(session_id);
+        if (it != sAveragedRangingData.end()) {
+          sAveragedRangingData.erase(session_id);
+          JNI_TRACE_E("%s: deinit: Averaging Disabled for Session %d", fn,
+                      session_id);
+        }
+      }
+      uwbEventManager.onSessionStatusNotificationReceived(
+          eventData->sSessionStatus.session_id, eventData->sSessionStatus.state,
+          eventData->sSessionStatus.reason_code);
+    }
+    break;
+  case UWA_DM_SESSION_SET_CONFIG_RSP_EVT: // result of UWA_SetAppConfig
+    JNI_TRACE_I("%s: UWA_DM_SESSION_SET_CONFIG_RSP_EVT", fn);
+    {
+      SyncEventGuard guard(sUwaSetAppConfigEvent);
+      sSetAppConfigRespStatus = true;
+      sSetAppConfigStatus = eventData->status;
+      sSetAppConfigLen = eventData->sApp_set_config.tlv_size;
+      sNoOfAppConfigIds = eventData->sApp_set_config.num_param_id;
+      if (eventData->sApp_set_config.tlv_size > 0) {
+        memcpy(sSetAppConfig, eventData->sApp_set_config.param_ids,
+               eventData->sApp_set_config.tlv_size);
+      }
+      sUwaSetAppConfigEvent.notifyOne();
+    }
+    break;
+  case UWA_DM_SESSION_GET_CONFIG_RSP_EVT: /* Result of UWA_GetAppConfig */
+    JNI_TRACE_I("%s: UWA_DM_SESSION_GET_CONFIG_RSP_EVT", fn);
+    {
+      SyncEventGuard guard(sUwaGetAppConfigEvent);
+      sGetAppConfigRespStatus = true;
+      sGetAppConfigStatus = eventData->status;
+      sGetAppConfigLen = eventData->sApp_get_config.tlv_size;
+      sNoOfAppConfigIds = eventData->sApp_get_config.no_of_ids;
+      if (eventData->sApp_get_config.tlv_size > 0) {
+        memcpy(sGetAppConfig, eventData->sApp_get_config.param_tlvs,
+               eventData->sApp_get_config.tlv_size);
+      }
+      sUwaGetAppConfigEvent.notifyOne();
+    }
+    break;
+  case UWA_DM_RANGE_START_RSP_EVT: /* result of range start command */
+    JNI_TRACE_I("%s: UWA_DM_RANGE_START_RSP_EVT", fn);
+    {
+      SyncEventGuard guard(sUwaRngStartEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        sRangeStartStatus = true;
+        JNI_TRACE_I("%s: UWA_DM_RANGE_START_RSP_EVT Success", fn);
+      } else {
+        sRangeStartStatus = false;
+        JNI_TRACE_E("%s: UWA_DM_RANGE_START_RSP_EVT failed", fn);
+      }
+      sUwaRngStartEvent.notifyOne();
+    }
+    break;
+  case UWA_DM_RANGE_STOP_RSP_EVT: /* result of range stop command */
+    JNI_TRACE_I("%s: UWA_DM_RANGE_STOP_RSP_EVT", fn);
+    {
+      SyncEventGuard guard(sUwaRngStopEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        sRangeStopStatus = true;
+        JNI_TRACE_I("%s: UWA_DM_RANGE_STOP_RSP_EVT Success", fn);
+      } else {
+        sRangeStopStatus = false;
+        JNI_TRACE_E("%s: UWA_DM_RANGE_STOP_RSP_EVT failed", fn);
+      }
+      sUwaRngStopEvent.notifyOne();
+    }
+    break;
+  case UWA_DM_GET_RANGE_COUNT_RSP_EVT:
+    JNI_TRACE_I("%s: UWA_DM_GET_RANGE_COUNT_RSP_EVT", fn);
+    {
+      SyncEventGuard guard(sUwaGetRangingCountEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        sRangingCount = eventData->sGet_range_cnt.count;
+      } else {
+        JNI_TRACE_E("%s: get range count Request is failed", fn);
+        sRangingCount = 0;
+      }
+      sUwaGetRangingCountEvent.notifyOne();
+    }
+    break;
+  case UWA_DM_RANGE_DATA_NTF_EVT:
+    JNI_TRACE_I("%s: UWA_DM_RANGE_DATA_NTF_EVT", fn);
+    { notifyRangeDataNotification(&eventData->sRange_data); }
+    break;
+  case UWA_DM_SESSION_GET_COUNT_RSP_EVT:
+    JNI_TRACE_I("%s: UWA_DM_SESSION_GET_COUNT_RSP_EVT", fn);
+    {
+      SyncEventGuard guard(sUwaGetSessionCountEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        sSessionCount = eventData->sGet_session_cnt.count;
+      } else {
+        JNI_TRACE_E("%s: get session count Request is failed", fn);
+        sSessionCount = -1;
+      }
+      sUwaGetSessionCountEvent.notifyOne();
+    }
+    break;
+
+  case UWA_DM_SESSION_GET_STATE_RSP_EVT:
+    JNI_TRACE_I("%s: UWA_DM_SESSION_GET_STATE_RSP_EVT", fn);
+    {
+      SyncEventGuard guard(sUwaGetSessionStatusEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        sSessionState = eventData->sGet_session_state.session_state;
+      } else {
+        JNI_TRACE_E("%s: get session state Request is failed", fn);
+        sSessionState = UWB_UNKNOWN_SESSION;
+      }
+      sUwaGetSessionStatusEvent.notifyOne();
+    }
+    break;
+
+  case UWA_DM_SESSION_MC_LIST_UPDATE_RSP_EVT: /* result of session update
+                                                 multicast list */
+    JNI_TRACE_I("%s: UWA_DM_SESSION_MC_LIST_UPDATE_RSP_EVT", fn);
+    {
+      SyncEventGuard guard(sUwaMulticastListUpdateEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        sMulticastListUpdateStatus = true;
+        JNI_TRACE_I("%s: UWA_DM_SESSION_MC_LIST_UPDATE_RSP_EVT Success", fn);
+      } else {
+        JNI_TRACE_E("%s: UWA_DM_SESSION_MC_LIST_UPDATE_RSP_EVT failed", fn);
+      }
+      sUwaMulticastListUpdateEvent.notifyOne();
+    }
+    break;
+
+  case UWA_DM_SESSION_MC_LIST_UPDATE_NTF_EVT:
+    JNI_TRACE_I("%s: UWA_DM_SESSION_MC_LIST_UPDATE_NTF_EVT", fn);
+    {
+      uwbEventManager.onMulticastListUpdateNotificationReceived(
+          &eventData->sMulticast_list_ntf);
+    }
+    break;
+
+  case UWA_DM_SET_COUNTRY_CODE_RSP_EVT:
+    JNI_TRACE_I("%s: UWA_DM_COUNTRY_CODE_UPDATE_RSP_EVT", fn);
+    {
+      SyncEventGuard guard(sUwaSetCountryCodeEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        sSetCountryCodeStatus = true;
+        JNI_TRACE_I("%s: UWA_DM_COUNTRY_CODE_UPDATE_RSP_EVT Success", fn);
+      } else {
+        JNI_TRACE_E("%s: UWA_DM_COUNTRY_CODE_UPDATE_RSP_EVT failed", fn);
+      }
+      sUwaSetCountryCodeEvent.notifyOne();
+    }
+    break;
+
+  case UWA_DM_SEND_BLINK_DATA_RSP_EVT:
+    JNI_TRACE_I("%s: UWA_DM_SEND_BLINK_DATA_RSP_EVT", fn);
+    {
+      SyncEventGuard guard(sUwaSendBlinkDataEvent);
+      sSendBlinkDataStatus = eventData->status;
+      sUwaSendBlinkDataEvent.notifyOne();
+    }
+    break;
+
+  case UWA_DM_GET_CORE_DEVICE_CAP_RSP_EVT:
+    JNI_TRACE_D("%s: UWA_DM_API_CORE_GET_DEVICE_CAPABILITY_EVT", fn);
+    {
+     SyncEventGuard guard(sUwaGetDeviceCapsEvent);
+     sGetDeviceCapsRespStatus = true;
+     sDevCapInfoLen = 0;
+     if (eventData->sGet_device_capability.status == UWA_STATUS_OK) {
+        sDevCapInfoIds = eventData->sGet_device_capability.no_of_tlvs;
+        sDevCapInfoLen = eventData->sGet_device_capability.tlv_buffer_len;
+        if (eventData->sGet_device_capability.tlv_buffer_len > 0 && (eventData->sGet_device_capability.tlv_buffer_len <= UCI_MAX_PKT_SIZE)) {
+            memcpy(sUwbDeviceCapability, eventData->sGet_device_capability.tlv_buffer, eventData->sGet_device_capability.tlv_buffer_len);
+        }
+     }
+     sUwaGetDeviceCapsEvent.notifyOne();
+     }
+    break;
+  case UWA_DM_SEND_BLINK_DATA_NTF_EVT:
+    JNI_TRACE_I("%s: UWA_DM_SEND_BLINK_DATA_NTF_EVT", fn);
+    {
+      uwbEventManager.onBlinkDataTxNotificationReceived(
+          eventData->sBlink_data_ntf.repetition_count_status);
+    }
+    break;
+  case UWA_VENDOR_SPECIFIC_UCI_NTF_EVT:
+     JNI_TRACE_I("%s: UWA_VENDOR_SPECIfIC_UCI_NTF_EVT", fn);
+     {
+      uint8_t mt, pbf, gid, oid, *ntf_data, *p_ntf_hdr;
+      uint16_t len = eventData->sVendor_specific_ntf.len-UCI_MSG_HDR_SIZE;
+      p_ntf_hdr = eventData->sVendor_specific_ntf.data;
+      ntf_data = (uint8_t *) eventData->sVendor_specific_ntf.data + UCI_MSG_HDR_SIZE;
+      UCI_MSG_PRS_HDR0(p_ntf_hdr, mt, pbf, gid);
+      UCI_MSG_PRS_HDR1(p_ntf_hdr, oid);
+      uwbEventManager.onVendorUciNotificationReceived(gid, oid,
+       ntf_data, len);
+     }
+     break;
+  case UWA_DM_CONFORMANCE_NTF_EVT:
+    JNI_TRACE_I("%s: UWA_DM_CONFORMANCE_NTF_EVT", fn);
+    {
+      uwbEventManager.onRawUciNotificationReceived(eventData->sConformance_ntf.data,
+      eventData->sConformance_ntf.length);
+    }
+    break;
+  case UWA_DM_CORE_GEN_ERR_STATUS_EVT:
+    JNI_TRACE_I("%s: UWA_DM_CORE_GEN_ERR_STATUS_EVT", fn);
+    {
+      uwbEventManager.onCoreGenericErrorNotificationReceived(
+          eventData->sCore_gen_err_status.status);
+    }
+    break;
+
+    //    case UWA_DM_UWBS_RESP_TIMEOUT_EVT:
+    //      JNI_TRACE_I("%s: UWA_DM_UWBS_RESP_TIMEOUT_EVT", fn);
+    //      {
+    //        sErrNotify.notifyAll();
+    //        sDeviceState = UWBS_STATUS_TIMEOUT;
+    //        uwbEventManager.onDeviceStateNotificationReceived(sDeviceState);
+    //      }
+    //      break;
+  default:
+    JNI_TRACE_I("%s: unhandled event", fn);
+    break;
+  }
+}
+
+/*******************************************************************************
+**
+** Function:        CommandResponse_Cb
+**
+** Description:     Receive response from the stack for raw command sent from
+*                   jni.
+**
+**                  event:  event ID.
+**                  paramLength: length of the response
+**                  pResponseBuffer: pointer to data
+**
+** Returns:         None
+**
+*******************************************************************************/
+static void CommandResponse_Cb(uint8_t event, uint16_t paramLength,
+                               uint8_t *pResponseBuffer) {
+  JNI_TRACE_I("%s: Entry", __func__);
+
+  if ((paramLength > UCI_RESPONSE_STATUS_OFFSET) && (pResponseBuffer != NULL)) {
+    JNI_TRACE_I("CommandResponse_Cb Received length data = 0x%x status = 0x%x",
+                paramLength, pResponseBuffer[UCI_RESPONSE_STATUS_OFFSET]);
+   sSendRawResLen = paramLength-UCI_MSG_HDR_SIZE;
+   memcpy(sSendRawResData, pResponseBuffer+UCI_MSG_HDR_SIZE, sSendRawResLen);
+  } else {
+    JNI_TRACE_E("%s:CommandResponse_Cb responseBuffer is NULL or Length < "
+                "UCI_RESPONSE_STATUS_OFFSET",
+                __func__);
+  }
+  SyncEventGuard guard(sUwaSendRawUciEvt);
+  sUwaSendRawUciEvt.notifyOne();
+
+  JNI_TRACE_I("%s: Exit", __func__);
+}
+
+/*******************************************************************************
+**
+** Function:        setAppConfiguration
+**
+** Description:     Set the session specific App Config
+**
+** Params:          session_id: Session Id of the required App Config
+**                  noOfParams: Number of Params need to configure
+**                  paramLen: Total Params Lentgh
+**                  appConfigParams: AppConfigs List in TLV format
+**
+** Returns:         If Success UWA_STATUS_OK  else UWA_STATUS_FAILED
+**
+*******************************************************************************/
+static tUWA_STATUS setAppConfiguration(uint32_t session_id, uint8_t noOfParams,
+                                       uint8_t paramLen,
+                                       uint8_t appConfigParams[]) {
+  static const char fn[] = "setAppConfiguration";
+  UNUSED(fn);
+  tUWA_STATUS status;
+  sSetAppConfigRespStatus = false;
+  SyncEventGuard guard(sUwaSetAppConfigEvent);
+  status = UWA_SetAppConfig(session_id, noOfParams, paramLen, appConfigParams);
+  if (status == UWA_STATUS_OK) {
+    sUwaSetAppConfigEvent.wait(UWB_CMD_TIMEOUT);
+    JNI_TRACE_I("%s: Success UWA_SetAppConfig Command", fn);
+  } else {
+    JNI_TRACE_E("%s: Failed UWA_SetAppConfig Command", fn);
+    return UWA_STATUS_FAILED;
+  }
+  return (sSetAppConfigRespStatus) ? UWA_STATUS_OK : UWA_STATUS_FAILED;
+}
+
+/*******************************************************************************
+**
+** Function:        sendRawUci
+**
+** Description:     Invoked this API to send raw uci cmds
+**
+** Params:          rawCmd: Ponter to the raw uci command
+**                  cmdLen: Length of the command
+**                  rsoData: Pointer to the response for sendRawUci cmd
+**                  rspLen: Length of response
+**
+** Returns:         If Success UWA_STATUS_OK  else UWA_STATUS_FAILED
+**
+*******************************************************************************/
+static tUWA_STATUS sendRawUci(uint8_t gid, uint8_t oid, uint8_t *rawCmd, uint16_t cmdLen) {
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+  uint8_t* pp;
+  uint8_t* p;
+  uint16_t len = cmdLen+UCI_MSG_HDR_SIZE;
+  p = pp =  (uint8_t *) phUwb_GKI_getbuf(len);
+
+  if (pp != NULL) {
+     SyncEventGuard guard(sUwaSendRawUciEvt);
+     UCI_MSG_BLD_HDR0(pp, UCI_MT_CMD, gid);
+     UCI_MSG_BLD_HDR1(pp, oid);
+     UINT8_TO_STREAM(pp, 0x00);
+     if (cmdLen ==1 && rawCmd[0] == 0  ) {
+       UINT8_TO_STREAM(pp, 0);
+       ARRAY_TO_STREAM(pp, rawCmd, 1);
+     } else {
+       UINT8_TO_STREAM(pp,cmdLen);
+       ARRAY_TO_STREAM(pp, rawCmd, cmdLen);
+     }
+
+     status = UWA_SendRawCommand(len, p, CommandResponse_Cb);
+     phUwb_GKI_freebuf(p);
+
+     if (status == UWA_STATUS_OK) {
+       JNI_TRACE_I("%s: Success UWA_SendRawCommand", __func__);
+        sUwaSendRawUciEvt.wait(UWB_CMD_TIMEOUT);
+     } else {
+       JNI_TRACE_E("%s: Failed UWA_SendRawCommand", __func__);
+       return status;
+     }
+  }
+
+  JNI_TRACE_I("%s: Exit", __func__);
+  return status;
+}
+
+/*******************************************************************************
+**
+** Function:        SetCoreDeviceConfigurations
+**
+** Description:     Set the Core Device Config
+**
+**
+** Returns:         If Success UWA_STATUS_OK  else UWA_STATUS_FAILED
+**
+*******************************************************************************/
+static tUWA_STATUS SetCoreDeviceConfigurations() {
+  uint8_t coreConfigsCount = 1;
+  static const char fn[] = "SetCoreDeviceConfigurations";
+  UNUSED(fn);
+  tUWA_STATUS status;
+  uint8_t configParam[] = {0x00, 0x00, 0x00};
+  uint16_t config = 0;
+  JNI_TRACE_I("%s: Enter ", fn);
+
+  config = UwbConfig::getUnsigned(NAME_UWB_LOW_POWER_MODE, 0x00);
+  JNI_TRACE_I("%s: NAME_UWB_LOW_POWER_MODE value %d ", fn, (uint8_t)config);
+
+  configParam[0] = (uint8_t)config;
+
+  {
+    SyncEventGuard guard(sUwaSetConfigEvent);
+    status = UWA_SetCoreConfig(UCI_PARAM_ID_LOW_POWER_MODE, coreConfigsCount,
+                               &configParam[0]);
+    if (status == UWA_STATUS_OK) {
+      sUwaSetConfigEvent.wait(UWB_CMD_TIMEOUT);
+      JNI_TRACE_I("%s: low power mode config is success", fn);
+    } else {
+      JNI_TRACE_E("%s: low power mode config is failed", fn);
+      return UWA_STATUS_FAILED;
+    }
+  }
+
+  JNI_TRACE_I("%s: Exit ", fn);
+  return status;
+}
+
+/*******************************************************************************
+**
+** Function:        clearAllSessionContext
+**
+** Description:     This API is invoked before Init and during DeInit to clear
+**                  All the Session specific context.
+**
+** Returns:         Nothing
+**
+*******************************************************************************/
+void clearAllSessionContext() {
+  {
+    std::unique_lock<std::mutex> lock(sSessionMutex);
+    sAveragedRangingData.clear();
+  }
+  clearRfTestContext();
+}
+
+/*******************************************************************************
+**
+** Function:        UwbDeviceReset
+**
+** Description:     Send Device Reset Command.
+**
+** Params:          resetConfig: Manufacturer/Vendor Specific Reset Data
+
+** Returns:         If Success UWA_STATUS_OK  else UWA_STATUS_FAILED
+**
+*******************************************************************************/
+bool UwbDeviceReset(uint8_t resetConfig) {
+  static const char fn[] = "UwbDeviceReset";
+  UNUSED(fn);
+  tUWA_STATUS status;
+  JNI_TRACE_I("%s: Enter", fn);
+
+  sIsDeviceResetDone = false;
+  {
+    SyncEventGuard guard(sUwaDeviceResetEvent);
+    status = UWA_SendDeviceReset((uint8_t)resetConfig);
+    if (status == UWA_STATUS_OK)
+      sUwaDeviceResetEvent.wait(UWB_CMD_TIMEOUT); /* wait for callback */
+  }
+  if (status == UWA_STATUS_OK) {
+    JNI_TRACE_E("%s: Success UWA_SendDeviceReset", fn);
+    if (sIsDeviceResetDone) {
+      SyncEventGuard guard(sUwadeviceNtfEvent);
+      sUwadeviceNtfEvent.wait(UWB_CMD_TIMEOUT);
+      switch (sDeviceState) {
+      case UWBS_STATUS_READY: {
+        clearAllSessionContext();
+        JNI_TRACE_I("%s: Device Reset is success %d", fn, sDeviceState);
+      } break;
+      default: {
+        JNI_TRACE_E("%s: Device state is = %d", fn, sDeviceState);
+      } break;
+      }
+    }
+  } else {
+    JNI_TRACE_E("%s: Failed UWA_SendDeviceReset", fn);
+  }
+  JNI_TRACE_I("%s: Exit", fn);
+  return sIsDeviceResetDone ? TRUE : FALSE;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_doInitialize
+**
+** Description:     Turn on UWB. initialize the GKI module and HAL module for
+*UWB device.
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**
+** Returns:         True if UWB device initialization is success.
+**
+*******************************************************************************/
+jboolean uwbNativeManager_doInitialize(JNIEnv *env, jobject o) {
+  static const char fn[] = "uwbNativeManager_doInitialize";
+  UNUSED(fn);
+  tUWA_STATUS status;
+  uint8_t resetConfig = 0;
+  JNI_TRACE_I("%s: enter", fn);
+
+  if (gIsUwaEnabled) {
+    JNI_TRACE_I("%s: Already Initialized", fn);
+    UwbDeviceReset(resetConfig);
+    return JNI_TRUE;
+  }
+
+  sDeviceState = UWBS_STATUS_ERROR;
+  UwbAdaptation &theInstance = UwbAdaptation::GetInstance();
+  theInstance.Initialize(); // start GKI, UCI task, UWB task
+  tHAL_UWB_ENTRY *halFuncEntries = theInstance.GetHalEntryFuncs();
+  UWA_Init(halFuncEntries);
+  clearAllSessionContext();
+  {
+    SyncEventGuard guard(sUwaEnableEvent);
+    status = UWA_Enable(uwaDeviceManagementCallback,
+                        uwaRfTestDeviceManagementCallback);
+    if (status == UWA_STATUS_OK)
+      sUwaEnableEvent.wait(UWB_CMD_TIMEOUT);
+  }
+  if (status == UWA_STATUS_OK) {
+    if (!gIsUwaEnabled) {
+      JNI_TRACE_E("%s: UWB Enable failed", fn);
+      goto error;
+    }
+    status = theInstance.CoreInitialization();
+    JNI_TRACE_I("%s: CoreInitialization status: %d", fn, status);
+
+    if (status == UWA_STATUS_OK) {
+#if 1 // WA waiting binding status NTF/ SE comm error NTF
+#endif
+      {
+        /* Get Device Info */
+        {
+          SyncEventGuard guard(sUwaGetDeviceInfoEvent);
+          status = UWA_GetDeviceInfo();
+
+          if (status == UWA_STATUS_OK) {
+            sUwaGetDeviceInfoEvent.wait();
+            JNI_TRACE_I("UCI Version : %x.%x",
+                        (sUwbDeviceInfo.uciVersion & 0X00FF),
+                        (sUwbDeviceInfo.uciVersion >> 8));
+          }
+        }
+
+        if (status == UWA_STATUS_OK) {
+          gIsUwaEnabled = true;
+          status = SetCoreDeviceConfigurations();
+          if (status == UWA_STATUS_OK) {
+            JNI_TRACE_I("%s: SetCoreDeviceConfigurations is SUCCESS %d", fn,
+                        status);
+          } else {
+            JNI_TRACE_I("%s: SetCoreDeviceConfigurations is Failed %d", fn,
+                        status);
+            goto error;
+          }
+          goto end;
+        }
+      }
+    }
+  }
+error:
+  JNI_TRACE_E("%s: device status is failed %d", fn, sDeviceState);
+  gIsUwaEnabled = false;
+  status = UWA_Disable(false); /* gracefull exit */
+  if (status == UWA_STATUS_OK) {
+    JNI_TRACE_I("%s: UWA_Disable(false) SUCCESS %d", fn, status);
+  } else {
+    JNI_TRACE_E("%s: UWA_Disable(false) is failed %d", fn, status);
+  }
+  theInstance.Finalize(false); // disable GKI, UCI task, UWB task
+end:
+  if (gIsUwaEnabled) {
+    sDeviceState = UWBS_STATUS_READY;
+  }
+  JNI_TRACE_I("%s: exit", fn);
+  return gIsUwaEnabled ? JNI_TRUE : JNI_FALSE;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_doDeinitialize
+**
+** Description:     Turn off UWB. Deinitilize the GKI and HAL module, power
+**                  of the UWB device.
+** Params:          env: JVM environment.
+**                  o: Java object.
+**
+** Returns:         True if UWB device De-initialization is success.
+**
+*******************************************************************************/
+jboolean uwbNativeManager_doDeinitialize(JNIEnv *env, jobject obj) {
+  static const char fn[] = "uwbNativeManager_doDeinitialize";
+  UNUSED(fn);
+  tUWA_STATUS status;
+  JNI_TRACE_I("%s: Enter", fn);
+  UwbAdaptation &theInstance = UwbAdaptation::GetInstance();
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is already De-initialized", fn);
+    return JNI_TRUE;
+  }
+
+  SyncEventGuard guard(sUwaDisableEvent);
+  status = UWA_Disable(true); /* gracefull exit */
+  if (status == UWA_STATUS_OK) {
+    JNI_TRACE_I("%s: wait for de-init completion:", fn);
+    sUwaDisableEvent.wait();
+  } else {
+    JNI_TRACE_E("%s: De-Init is failed:", fn);
+  }
+  clearAllSessionContext();
+  gIsUwaEnabled = false;
+  theInstance.Finalize(true); // disable GKI, UCI task, UWB task
+  JNI_TRACE_I("%s: Exit", fn);
+  return JNI_TRUE;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_getDeviceInfo
+**
+** Description:     retrieve the UWB device information etc.
+** Params:          env: JVM environment.
+**                  o: Java object.
+**
+** Returns:         device info class object or NULL.
+**
+*******************************************************************************/
+jobject uwbNativeManager_getDeviceInfo(JNIEnv *env, jobject obj) {
+  static const char fn[] = "uwbNativeManager_getDeviceInfo";
+  UNUSED(fn);
+  JNI_TRACE_I("%s: Enter", fn);
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", fn);
+    return NULL;
+  }
+
+  // TODO need to change this implemenatation based on service.
+  const char *DEVICE_DATA_CLASS_NAME = "com/android/server/uwb/UwbDeviceData";
+  jclass deviceDataClass = env->FindClass(DEVICE_DATA_CLASS_NAME);
+  jmethodID constructor =
+      env->GetMethodID(deviceDataClass, "<init>",
+                       "(IIII)V"); // TODO to be updated based on service
+  if (constructor == JNI_NULL) {
+    JNI_TRACE_E("%s: jni cannot find the method deviceInfoClass", fn);
+    return NULL;
+  }
+
+  jint uciVersion = sUwbDeviceInfo.uciVersion;
+  jint uciTestVersion = sUwbDeviceInfo.uciTestVersion;
+  jint macVersion = sUwbDeviceInfo.macVersion;
+  jint phyVersion = sUwbDeviceInfo.phyVersion;
+
+  JNI_TRACE_I("%s: Exit", fn);
+
+  return env->NewObject(deviceDataClass, constructor, uciVersion, macVersion,
+                        phyVersion, uciTestVersion);
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_getSpecificationInfo
+**
+** Description:     retrieve the UWB device specific information etc.
+** Params:          env: JVM environment.
+**                  o: Java object.
+**
+** Returns:         device info class object or NULL.
+**
+*******************************************************************************/
+jobject uwbNativeManager_getSpecificationInfo(JNIEnv *env, jobject obj) {
+  static const char fn[] = "uwbNativeManager_getSpecificationInfo";
+  UNUSED(fn);
+  JNI_TRACE_I("%s: Enter", fn);
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", fn);
+    return NULL;
+  }
+
+  const char *DEVICE_DATA_CLASS_NAME =
+      "com/android/server/uwb/info/UwbSpecificationInfo";
+  jclass deviceDataClass = env->FindClass(DEVICE_DATA_CLASS_NAME);
+  jmethodID constructor =
+      env->GetMethodID(deviceDataClass, "<init>", "(IIIIIIIIIIIIIIII)V");
+  if (constructor == JNI_NULL) {
+    JNI_TRACE_E("%s: jni cannot find the method deviceInfoClass", fn);
+    return NULL;
+  }
+
+  jint uciMajor = (sUwbDeviceInfo.uciVersion & MSB_BITMASK);
+  jint uciMaintenance = (sUwbDeviceInfo.uciVersion >> 8) & 0x0F;
+  jint uciMinor = (sUwbDeviceInfo.uciVersion >> 12) & 0x0F;
+  jint macMajor = (sUwbDeviceInfo.macVersion & MSB_BITMASK);
+  jint macMaintenance = (sUwbDeviceInfo.macVersion >> 8) & 0x0F;
+  jint macMinor = (sUwbDeviceInfo.macVersion >> 12) & 0x0F;
+  jint phyMajor = (sUwbDeviceInfo.phyVersion & MSB_BITMASK);
+  jint phyMaintenance = (sUwbDeviceInfo.phyVersion >> 8) & 0x0F;
+  jint phyMinor = (sUwbDeviceInfo.phyVersion >> 12) & 0x0F;
+  jint uciTestMajor = (sUwbDeviceInfo.uciTestVersion) & MSB_BITMASK;
+  jint uciTestMaintenance = (sUwbDeviceInfo.uciTestVersion >> 8) & 0x0F;
+  jint uciTestMinor = (sUwbDeviceInfo.uciTestVersion >> 12) & 0x0F;
+
+  JNI_TRACE_I("%s: Exit", fn);
+
+  return env->NewObject(deviceDataClass, constructor, uciMajor, uciMaintenance,
+                        uciMinor, macMajor, macMaintenance, macMinor, phyMajor,
+                        phyMaintenance, phyMinor, uciTestMajor,
+                        uciTestMaintenance, uciTestMinor,
+                        1 /* firaMajorVersion */, 0 /* firaMinorVersion */,
+                        1 /* cccMajorVersion */, 0 /* cccMinorVersion*/);
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_getUwbDeviceState
+**
+** Description:     Retrieve the UWB device state..
+**
+** Params :         env: JVM environment.
+**                  o: Java object.
+**
+** Returns:         device state.
+**
+*******************************************************************************/
+jint uwbNativeManager_getUwbDeviceState(JNIEnv *env, jobject obj) {
+  static const char fn[] = "uwbNativeManager_getUwbDeviceState";
+  UNUSED(fn);
+  eUWBS_DEVICE_STATUS_t sDeviceState = UWBS_STATUS_ERROR;
+  JNI_TRACE_I("%s: Enter", fn);
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", fn);
+    return sDeviceState;
+  }
+
+  tUWA_PMID configParam[] = {UCI_PARAM_ID_DEVICE_STATE};
+  SyncEventGuard guard(sUwaGetConfigEvent);
+  tUWA_STATUS status = UWA_GetCoreConfig(sizeof(configParam), configParam);
+  if (status == UWA_STATUS_OK) {
+    sUwaGetConfigEvent.wait(UWB_CMD_TIMEOUT);
+    if (sGetCoreConfigLen > 0) {
+      if (sGetCoreConfig[0] == UCI_PARAM_ID_DEVICE_STATE) {
+        sDeviceState = (eUWBS_DEVICE_STATUS_t)sGetCoreConfig[2];
+      }
+    }
+  }
+  JNI_TRACE_I("%s: Exit", fn);
+  return sDeviceState;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_deviceReset
+**
+** Description:     Send Device Reset Command.
+**
+** Params:          env: JVM environment.
+**                  obj: Java object.
+**                  resetConfig: Manufacturer/Vendor Specific Reset Data
+**
+** Returns:         If Success UWA_STATUS_OK  else UWA_STATUS_FAILED
+**
+*******************************************************************************/
+jbyte uwbNativeManager_deviceReset(JNIEnv *env, jobject obj,
+                                   jbyte resetConfig) {
+  static const char fn[] = "uwbNativeManager_deviceReset";
+  UNUSED(fn);
+  bool status;
+  JNI_TRACE_I("%s: Enter", fn);
+
+  // WA: commented reset functionality as this wiill trigger ESE communication
+  // and Helios will send binding status NTF again
+  // if Helos is turned off without reading response from ESE, then this makes
+  // ESE unresponsive sitiation
+  // Sending reset command as part of MW enable every time to reset both Helios
+  // and SUS applet from ESE
+  status = true; // true always
+#if 0
+  if(!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", fn);
+    return UWA_STATUS_FAILED;
+  }
+
+  status = UwbDeviceReset((uint8_t)resetConfig);
+#endif
+  JNI_TRACE_I("%s: Exit", fn);
+  return status ? UWA_STATUS_OK : UWA_STATUS_FAILED;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_sessionInit
+**
+** Description:     Initialize the session for the particular activity.
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**
+** Returns:         If Success UWA_STATUS_OK  else UWA_STATUS_FAILED
+**
+*******************************************************************************/
+jbyte uwbNativeManager_sessionInit(JNIEnv *env, jobject o, jint sessionId,
+                                   jbyte sessionType) {
+  static const char fn[] = "uwbNativeManager_sessionInit";
+  UNUSED(fn);
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+  JNI_TRACE_I("%s: Enter", fn);
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", fn);
+    return status;
+  }
+
+  sSessionInitStatus = false;
+  SyncEventGuard guard(sUwaSessionInitEvent);
+  status = UWA_SendSessionInit(sessionId, sessionType);
+  if (UWA_STATUS_OK == status) {
+    sUwaSessionInitEvent.wait(UWB_CMD_TIMEOUT);
+  } else {
+    JNI_TRACE_E("%s: Session Init command is  failed", fn);
+  }
+
+  JNI_TRACE_I("%s: Exit", fn);
+  return (sSessionInitStatus) ? UWA_STATUS_OK : UWA_STATUS_FAILED;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_sessionDeInit
+**
+** Description:     This API is invoked from the application to DeInitialize
+**                  Session Specific context.
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**
+** Returns:         If Success UWA_STATUS_OK  else UWA_STATUS_FAILED
+**
+*******************************************************************************/
+jbyte uwbNativeManager_sessionDeInit(JNIEnv *env, jobject o, jint sessionId) {
+  static const char fn[] = "uwbNativeManager_sessionDeInit";
+  UNUSED(fn);
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+  JNI_TRACE_I("%s: Enter", fn);
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", fn);
+    return status;
+  }
+
+  sSessionDeInitStatus = false;
+  SyncEventGuard guard(sUwaSessionDeInitEvent);
+  status = UWA_SendSessionDeInit(sessionId);
+  if (UWA_STATUS_OK == status) {
+    sUwaSessionDeInitEvent.wait(UWB_CMD_TIMEOUT);
+  } else {
+    JNI_TRACE_E("%s: Session DeInit command is  failed", fn);
+  }
+  JNI_TRACE_I("%s: Exit", fn);
+  return (sSessionDeInitStatus) ? UWA_STATUS_OK : UWA_STATUS_FAILED;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_setAppConfigurations()
+**
+** Description:     Invoked this API to set the session specific Application
+**                  Configuration.
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**                  sessionId: All APP configurations belonging to this Session
+*ID
+**                  noOfParams : The number of APP Configuration fields to
+*follow
+**                  appConfigLen : Length of AppConfigData
+**                  AppConfig : App Configurations for session
+**
+** Returns:         Returns byte array
+**
+*******************************************************************************/
+jobject uwbNativeManager_setAppConfigurations(JNIEnv *env, jobject o,
+                                                 jint sessionId,
+                                                 jint noOfParams,
+                                                 jint appConfigLen,
+                                                 jbyteArray AppConfig) {
+  static const char fn[] = "uwbNativeManager_setAppConfigurations";
+  UNUSED(fn);
+  uint8_t *appConfigData = NULL;
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+  JNI_TRACE_I("%s: Enter", fn);
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", fn);
+    return NULL;
+  }
+
+  appConfigData = (uint8_t *)malloc(sizeof(uint8_t) * appConfigLen);
+  if (appConfigData != NULL) {
+      memset(appConfigData, 0, (sizeof(uint8_t) * appConfigLen));
+      env->GetByteArrayRegion(AppConfig, 0, appConfigLen, (jbyte *)appConfigData);
+      JNI_TRACE_I("%d: appConfigLen", appConfigLen);
+      status =
+          setAppConfiguration(sessionId, noOfParams, appConfigLen, appConfigData);
+      free(appConfigData);
+      if (sSetAppConfigRespStatus) {
+          const char *UWB_CONFIG_STATUS_DATA =
+                  "com/android/server/uwb/data/UwbConfigStatusData";
+          jclass configStatusDataClass = env->FindClass(UWB_CONFIG_STATUS_DATA);
+          jmethodID constructor =
+              env->GetMethodID(configStatusDataClass, "<init>", "(II[B)V");
+          if (constructor == JNI_NULL) {
+            JNI_TRACE_E("%s: jni cannot find the method for UwbTlvDATA", fn);
+            return NULL;
+          }
+          jbyteArray appConfigArray =
+              env->NewByteArray(sSetAppConfigLen);
+          env->SetByteArrayRegion(appConfigArray, 0, sSetAppConfigLen,
+                                  (jbyte *)&sSetAppConfig[0]);
+          return env->NewObject(configStatusDataClass, constructor, sSetAppConfigStatus, sNoOfAppConfigIds, appConfigArray);
+      } else {
+          JNI_TRACE_E("%s: Failed setAppConfigurations, Status = %d", fn,
+                    sSetAppConfigRespStatus);
+      }
+  } else {
+    JNI_TRACE_E("%s: Unable to Allocate Memory", fn);
+  }
+  JNI_TRACE_I("%s: Exit", fn);
+  return NULL;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_sendRawUci()
+**
+** Description:     Invoked this API to send raw uci cmds
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**                  rawUci: Uci data to send to controller
+**                  cmdLen: uci data lentgh
+**
+** Returns:         Returns byte array for raw uci rsp
+**
+*******************************************************************************/
+jobject uwbNativeManager_sendRawUci(JNIEnv *env, jobject o,
+                                       jint gid, jint oid,
+                                       jbyteArray rawUci) {
+  static const char fn[] = "uwbNativeManager_sendRawUci";
+  UNUSED(fn);
+  JNI_TRACE_I("%s: enter; ", fn);
+  uint8_t *cmd = NULL;
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+  jint cmdLen = env->GetArrayLength(rawUci);
+  if (cmdLen > UCI_MAX_PAYLOAD_SIZE) {
+    JNI_TRACE_E("%s: CmdLen %d is beyond max allowed range %d", fn, cmdLen,
+                UCI_MAX_PAYLOAD_SIZE);
+    return NULL;
+  }
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", fn);
+    return NULL;
+  }
+
+  cmd = (uint8_t *)malloc(sizeof(uint8_t) * cmdLen);
+  if (cmd == NULL) {
+    JNI_TRACE_E("%s: malloc failure for raw cmd", __func__);
+    return NULL;
+  }
+  memset(cmd, 0, (sizeof(uint8_t) * cmdLen));
+  env->GetByteArrayRegion(rawUci, 0, cmdLen, (jbyte *)cmd);
+
+  status = sendRawUci(gid, oid, cmd, cmdLen);
+  free(cmd);
+
+  const char *UWB_VENDOR_RES_DATA =
+          "com/android/server/uwb/data/UwbVendorUciResponse";
+  jclass resDataClass = env->FindClass(UWB_VENDOR_RES_DATA);
+  jmethodID constructor =
+      env->GetMethodID(resDataClass, "<init>", "(BII[B)V");
+  if (constructor == JNI_NULL) {
+    JNI_TRACE_E("%s: jni cannot find the method for UwbTlvDATA", fn);
+    return NULL;
+  }
+  JNI_TRACE_I("%s: exit sendRawUCi= 0x%x", __func__, status);
+  if (status == UWA_STATUS_OK) {
+     jbyteArray rawResArray =
+         env->NewByteArray(sSendRawResLen);
+     env->SetByteArrayRegion(rawResArray, 0, sSendRawResLen,
+                             (jbyte *)&sSendRawResData[0]);
+     return env->NewObject(resDataClass, constructor, status, gid, oid, rawResArray);
+  } else {
+     return env->NewObject(resDataClass, constructor, status, gid, oid, NULL);
+  }
+
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_getAppConfigurations
+**
+** Description:     retrieve the session specific App Configs
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**                  session id : Session Id for the given set of App params
+**                  noOfParams: Number of Params
+**                  appConfigLen: Total App config Lentgh
+**                  AppConfig: App Config List to retrieve
+**
+** Returns:         Returns byte array
+**
+*******************************************************************************/
+jobject uwbNativeManager_getAppConfigurations(JNIEnv *env, jobject o,
+                                                 jint sessionId,
+                                                 jint noOfParams,
+                                                 jint appConfigLen,
+                                                 jbyteArray AppConfig) {
+  static const char fn[] = "uwbNativeManager_getAppConfigurations";
+  UNUSED(fn);
+  tUWA_STATUS status;
+  uint8_t *appConfigData = NULL;
+  JNI_TRACE_I("%s: Enter", fn);
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", fn);
+    return NULL;
+  }
+
+  sGetAppConfigRespStatus = false;
+  appConfigData = (uint8_t *)malloc(sizeof(uint8_t) * appConfigLen);
+  if (appConfigData != NULL) {
+      memset(appConfigData, 0, (sizeof(uint8_t) * appConfigLen));
+      env->GetByteArrayRegion(AppConfig, 0, appConfigLen, (jbyte *)appConfigData);
+      SyncEventGuard guard(sUwaGetAppConfigEvent);
+      status =
+          UWA_GetAppConfig(sessionId, noOfParams, appConfigLen, appConfigData);
+      free(appConfigData);
+      if (status == UWA_STATUS_OK) {
+          sUwaGetAppConfigEvent.wait(UWB_CMD_TIMEOUT);
+          if (sGetAppConfigRespStatus) {
+              const char *UWB_TLV_DATA =
+                 "com/android/server/uwb/data/UwbTlvData";
+              jclass tlvDataClass = env->FindClass(UWB_TLV_DATA);
+              jmethodID constructor =
+                env->GetMethodID(tlvDataClass, "<init>", "(II[B)V");
+               if (constructor == JNI_NULL) {
+                   JNI_TRACE_E("%s: jni cannot find the method for UwbTlvDATA", fn);
+                   return NULL;
+               }
+               jbyteArray appConfigArray =
+                   env->NewByteArray(sGetAppConfigLen);
+               env->SetByteArrayRegion(appConfigArray, 0, sGetAppConfigLen,
+                                  (jbyte *)&sGetAppConfig[0]);
+               return env->NewObject(tlvDataClass, constructor, sGetAppConfigStatus, sNoOfAppConfigIds, appConfigArray);
+          } else {
+               JNI_TRACE_E("%s: Failed getAppConfigurations, Status = %d", fn,
+                      sGetAppConfigRespStatus);
+          }
+      } else {
+        JNI_TRACE_E("%s: Failed UWA_GetAppConfig", fn);
+      }
+  } else {
+      JNI_TRACE_E("%s: Unable to Allocate Memory", fn);
+  }
+  JNI_TRACE_I("%s: Exit", fn);
+  return NULL;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_startRanging
+**
+** Description:     start the ranging session with required configs and notify
+**                  the peer device information.
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**                  sessionId :  Session ID for which ranging shall start
+**
+** Returns:         If Success UWA_STATUS_OK  else UWA_STATUS_FAILED
+**
+*******************************************************************************/
+jbyte uwbNativeManager_startRanging(JNIEnv *env, jobject obj, jint sessionId) {
+  static const char fn[] = "uwbNativeManager_startRanging";
+  UNUSED(fn);
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+  JNI_TRACE_I("%s: enter", fn);
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not enabled", fn);
+    return status;
+  }
+
+  sRangeStartStatus = false;
+  SyncEventGuard guard(sUwaRngStartEvent);
+  status = UWA_StartRangingSession(sessionId);
+  if (status == UWA_STATUS_OK) {
+    sUwaRngStartEvent.wait(UWB_CMD_TIMEOUT);
+  }
+  JNI_TRACE_I("%s: exit", fn);
+  return (sRangeStartStatus) ? UWA_STATUS_OK : UWA_STATUS_FAILED;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_stopRanging
+**
+** Description:     stop the ranging session.
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**                  sessionId :  Session ID for which ranging shall start
+**
+** Returns:         UWA_STATUS_OK if ranging session stop is success.
+**
+*******************************************************************************/
+jbyte uwbNativeManager_stopRanging(JNIEnv *env, jobject obj, jint sessionId) {
+  static const char fn[] = "uwbNativeManager_stopRanging";
+  UNUSED(fn);
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+  JNI_TRACE_I("%s: enter", fn);
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not enabled", fn);
+    return status;
+  }
+
+  sRangeStopStatus = false;
+  SyncEventGuard guard(sUwaRngStopEvent);
+  status = UWA_StopRangingSession(sessionId);
+  if (status == UWA_STATUS_OK) {
+    sUwaRngStopEvent.wait(UWB_CMD_TIMEOUT);
+  } else {
+    JNI_TRACE_E("%s: Stop ranging is failed  error:%x:", fn, status);
+  }
+  JNI_TRACE_I("%s: exit", fn);
+  return (sRangeStopStatus) ? UWA_STATUS_OK : UWA_STATUS_FAILED;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_getSessionCount
+**
+** Description:     Get session count.
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**
+** Returns:         session count on success
+**
+*******************************************************************************/
+jbyte uwbNativeManager_getSessionCount(JNIEnv *env, jobject obj) {
+  static const char fn[] = "uwbNativeManager_getSessionCount";
+  UNUSED(fn);
+  tUWA_STATUS status;
+  sSessionCount = -1;
+  JNI_TRACE_I("%s: Enter", fn);
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", fn);
+    return sSessionCount;
+  }
+
+  SyncEventGuard guard(sUwaGetSessionCountEvent);
+  status = UWA_GetSessionCount();
+  if (UWA_STATUS_OK == status) {
+    sUwaGetSessionCountEvent.wait(UWB_CMD_TIMEOUT);
+  } else {
+    JNI_TRACE_E("%s: get session count command is  failed", fn);
+  }
+  JNI_TRACE_I("%s: Exit", fn);
+  return sSessionCount;
+}
+
+jint uwbNativeManager_getMaxSessionNumber(JNIEnv *env, jobject obj) {
+  static const char fn[] = "uwbNativeManager_getMaxSessionNumber";
+  UNUSED(fn);
+
+  return 5;
+}
+
+jbyte uwbNativeManager_resetDevice(JNIEnv *env, jbyte resetConfig) {
+  static const char fn[] = "uwbNativeManager_resetDevice";
+  UNUSED(fn);
+
+  return UWA_STATUS_OK;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_getSessionState
+**
+** Description:     get the current session status for the given session id
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**                  sessionId :  Session ID for which to get the session status
+**
+** Returns:         current session status if UWb_STATUS_OK else returns
+**                  UWA_STATUS_FAILED.
+**
+*******************************************************************************/
+jbyte uwbNativeManager_getSessionState(JNIEnv *env, jobject obj,
+                                       jint sessionId) {
+  static const char fn[] = "uwbNativeManager_getSessionState";
+  UNUSED(fn);
+  tUWA_STATUS status;
+  JNI_TRACE_I("%s: enter", fn);
+  sSessionState = UWB_UNKNOWN_SESSION;
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not enabled", fn);
+    return sSessionState;
+  }
+
+  SyncEventGuard guard(sUwaGetSessionStatusEvent);
+  status = UWA_GetSessionStatus(sessionId);
+  if (status == UWA_STATUS_OK) {
+    sUwaGetSessionStatusEvent.wait(UWB_CMD_TIMEOUT);
+  }
+  JNI_TRACE_I("%s: exit", fn);
+  return sSessionState;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_ControllerMulticastListUpdate()
+**
+** Description:     API to set Controller Multicast List Update
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**                  sessionId: Session Id to which update the list
+**                  action: Required Action to be taken
+**                  noOfControlees: Number of Responders
+**                  shortAddressList: Short Address list for each responder
+**                  subSessionIdList: Sub session Id list of each responder
+**
+** Returns:         UFA_STATUS_OK on success or UFA_STATUS_FAILED on failure
+**
+*******************************************************************************/
+jbyte uwbNativeManager_ControllerMulticastListUpdate(
+    JNIEnv *env, jobject o, jint sessionId, jbyte action, jbyte noOfControlees,
+    jshortArray shortAddressList, jintArray subSessionIdList) {
+  static const char fn[] = "uwbNativeManager_ControllerMulticastListUpdate";
+  UNUSED(fn);
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+  uint16_t *shortAddressArray = NULL;
+  uint32_t *subSessionIdArray = NULL;
+  JNI_TRACE_E("%s: enter; ", fn);
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", fn);
+    return status;
+  }
+
+  if ((shortAddressList == NULL) || (subSessionIdList == NULL)) {
+    JNI_TRACE_E("%s: subSessionIdList or shortAddressList value is NULL", fn);
+    return status;
+  }
+  uint8_t shortAddressLen = env->GetArrayLength(shortAddressList);
+  uint8_t subSessionIdLen = env->GetArrayLength(subSessionIdList);
+  if (noOfControlees > MAX_NUM_CONTROLLEES) {
+    JNI_TRACE_E("%s: no Of Controlees %d exceeded than %d ", fn,
+                shortAddressLen, MAX_NUM_CONTROLLEES);
+    return status;
+  }
+
+  if ((shortAddressLen > 0) && (subSessionIdLen > 0)) {
+    shortAddressArray = (uint16_t *)malloc(shortAddressLen);
+    if (shortAddressArray == NULL) {
+      JNI_TRACE_E("%s: malloc failure for shortAddressArray", fn);
+      return status;
+    }
+    memset(shortAddressArray, 0, shortAddressLen);
+    env->GetShortArrayRegion(shortAddressList, 0, shortAddressLen,
+                            (jshort *)shortAddressArray);
+
+    subSessionIdArray = (uint32_t *)malloc(SESSION_ID_LEN * subSessionIdLen);
+    if (subSessionIdArray == NULL) {
+      free(shortAddressArray);
+      JNI_TRACE_E("%s: malloc failure for subSessionIdArray", fn);
+      return status;
+    }
+    memset(subSessionIdArray, 0, (SESSION_ID_LEN * subSessionIdLen));
+    env->GetIntArrayRegion(subSessionIdList, 0, subSessionIdLen,
+                           (jint *)subSessionIdArray);
+
+    sMulticastListUpdateStatus = false;
+    SyncEventGuard guard(sUwaMulticastListUpdateEvent);
+    status = UWA_ControllerMulticastListUpdate(
+        sessionId, action, noOfControlees, shortAddressArray, subSessionIdArray);
+    if (status == UWA_STATUS_OK) {
+      sUwaMulticastListUpdateEvent.wait(UWB_CMD_TIMEOUT);
+    }
+    free(shortAddressArray);
+    free(subSessionIdArray);
+  } else {
+    JNI_TRACE_E("%s: controleeListArray length is not valid", fn);
+  }
+  JNI_TRACE_I("%s: exit", fn);
+  return (sMulticastListUpdateStatus) ? UWA_STATUS_OK : UWA_STATUS_FAILED;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_SetCountryCode()
+**
+** Description:     API to set country code
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**                  countryCode: ISO country code
+**
+** Returns:         UFA_STATUS_OK on success or UFA_STATUS_FAILED on failure
+**
+*******************************************************************************/
+jbyte uwbNativeManager_SetCountryCode(JNIEnv *env, jobject o,
+                                      jbyteArray countryCode) {
+  static const char fn[] = "uwbNativeManager_SetCountryCode";
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+  uint8_t *countryCodeArray = NULL;
+  JNI_TRACE_E("%s: enter; ", fn);
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", fn);
+    return status;
+  }
+  if (countryCode == NULL) {
+    JNI_TRACE_E("%s: country code value is NULL", fn);
+    return status;
+  }
+  uint8_t countryCodeArrayLen = env->GetArrayLength(countryCode);
+  if (countryCodeArrayLen != 2) {
+    JNI_TRACE_E("%s: Malformed country code arraylen %d", fn,
+                countryCodeArrayLen);
+    return status;
+  }
+
+  countryCodeArray = (uint8_t *)malloc(countryCodeArrayLen);
+  if (countryCodeArray == NULL) {
+    JNI_TRACE_E("%s: malloc failure for countryCodeArray", fn);
+    return status;
+  }
+  memset(countryCodeArray, 0, countryCodeArrayLen);
+  env->GetByteArrayRegion(countryCode, 0, countryCodeArrayLen,
+                          (jbyte *)countryCodeArray);
+  sSetCountryCodeStatus = false;
+  SyncEventGuard guard(sUwaSetCountryCodeEvent);
+  status = UWA_ControllerSetCountryCode(countryCodeArray);
+  if (status == UWA_STATUS_OK) {
+    sUwaSetCountryCodeEvent.wait(UWB_CMD_TIMEOUT);
+  }
+  free(countryCodeArray);
+  JNI_TRACE_I("%s: exit", fn);
+  return (sSetCountryCodeStatus) ? UWA_STATUS_OK : UWA_STATUS_FAILED;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_init
+**
+** Description:     Initialize variables.
+**
+** Params           env: JVM environment.
+**                  o: Java object.
+**
+** Returns:         True if ok.
+**
+*******************************************************************************/
+jboolean uwbNativeManager_init(JNIEnv *env, jobject o) {
+  uwbEventManager.doLoadSymbols(env, o);
+  return JNI_TRUE;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_enableConformanceTest
+**
+** Description:     Enable or disable MCTT mode of operation.
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**                  enable: enable/disable MCTT mode
+**
+********************************************************************************/
+jbyte uwbNativeManager_enableConformanceTest(JNIEnv *env, jobject o,
+                                             jboolean enable) {
+  static const char fn[] = "uwbNativeManager_enableConformanceTest";
+  UNUSED(fn);
+  JNI_TRACE_I("%s: enter", fn);
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not enabled", fn);
+    return status;
+  }
+  UWB_EnableConformanceTest(enable);
+  JNI_TRACE_I("%s: exit", fn);
+  return UWA_STATUS_OK;
+}
+
+/*******************************************************************************
+**
+** Function:        uwbNativeManager_GetDeviceCapebilityParams()
+**
+** Description:     Invoked this API to get the device capability information.
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**
+** Returns:         Returns byte array
+**
+*******************************************************************************/
+jobject uwbNativeManager_GetDeviceCapebilityParams(JNIEnv* env, jobject o) {
+  JNI_TRACE_I("%s: Entry", __func__);
+  tUWA_STATUS status;
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", __func__);
+    return NULL;
+  }
+
+  sGetDeviceCapsRespStatus = false;
+  SyncEventGuard guard(sUwaGetDeviceCapsEvent);
+  status = UWA_GetCoreGetDeviceCapability();
+  if (status == UWA_STATUS_OK) {
+    JNI_TRACE_D("%s: Success UWA_GetCoreGetDeviceCapability", __func__);
+    sUwaGetDeviceCapsEvent.wait(UWB_CMD_TIMEOUT);
+  } else {
+    JNI_TRACE_E("%s: Failed UWA_GetCoreGetDeviceCapability", __func__);
+    return NULL;
+  }
+
+  if (!sGetDeviceCapsRespStatus) {
+    JNI_TRACE_E("%s: Failed getDeviceCapabilityInfo, Status = %d", __func__,
+                sGetDeviceCapsRespStatus);
+    return NULL;
+  }
+
+  const char *UWB_TLV_DATA =
+	"com/android/server/uwb/data/UwbTlvData";
+  jclass tlvDataClass = env->FindClass(UWB_TLV_DATA);
+  jmethodID constructor =
+      env->GetMethodID(tlvDataClass, "<init>", "(II[B)V");
+  if (constructor == JNI_NULL) {
+    JNI_TRACE_E("%s: jni cannot find the method for UwbTlvDATA", __func__);
+    return NULL;
+  }
+
+  //remove vendor ext parameters
+  uint8_t sUwbDeviceCapaInfos[UCI_MAX_PKT_SIZE];
+  uint16_t capLen = 0;
+  for( uint16_t index= 0;index< sDevCapInfoLen;) {
+      if (sUwbDeviceCapability[index] == 0xE0 ) { //Ext id
+         switch (sUwbDeviceCapability[index+1]) { //Ext sub id
+             case 0x00:
+             case 0x01:
+             case 0x02:
+	       int lenofParam =  sUwbDeviceCapability[index+2]; //Get the length of Ext paramaeter
+	       index = index + (lenofParam+3); // increament index by (Ext id+ Ext sub id + length of ext param + velue of param)
+               sDevCapInfoIds--;
+	      break;
+	 }
+      } else {
+	 sUwbDeviceCapaInfos[capLen++] = sUwbDeviceCapability[index];
+         index++;
+      }
+  }
+  jbyteArray deviceCapabilityInfo = env->NewByteArray(capLen);
+  env->SetByteArrayRegion(deviceCapabilityInfo, 0, capLen,
+                          (jbyte*)&sUwbDeviceCapaInfos[0]);
+  JNI_TRACE_I("%s: Exit", __func__);
+  return env->NewObject(tlvDataClass, constructor, status, sDevCapInfoIds, deviceCapabilityInfo);
+}
+
+/*****************************************************************************
+**
+** JNI functions for android
+** UWB service layer has to invoke these APIs to get required functionality
+**
+*****************************************************************************/
+static JNINativeMethod gMethods[] = {
+    {"nativeInit", "()Z", (void *)uwbNativeManager_init},
+    {"nativeDoInitialize", "()Z", (void *)uwbNativeManager_doInitialize},
+    {"nativeDoDeinitialize", "()Z", (void *)uwbNativeManager_doDeinitialize},
+    {"nativeSessionInit", "(IB)B", (void *)uwbNativeManager_sessionInit},
+    {"nativeSessionDeInit", "(I)B", (void *)uwbNativeManager_sessionDeInit},
+    {"nativeSetAppConfigurations",
+      "(III[B)Lcom/android/server/uwb/data/UwbConfigStatusData;",
+     (void *)uwbNativeManager_setAppConfigurations},
+    {"nativeGetAppConfigurations",
+      "(III[B)Lcom/android/server/uwb/data/UwbTlvData;",
+     (void *)uwbNativeManager_getAppConfigurations},
+    {"nativeRangingStart", "(I)B", (void *)uwbNativeManager_startRanging},
+    {"nativeRangingStop", "(I)B", (void *)uwbNativeManager_stopRanging},
+    {"nativeGetSessionCount", "()B", (void *)uwbNativeManager_getSessionCount},
+    {"nativeGetSessionState", "(I)B", (void *)uwbNativeManager_getSessionState},
+    {"nativeControllerMulticastListUpdate", "(IBB[S[I)B",
+     (void *)uwbNativeManager_ControllerMulticastListUpdate},
+    {"nativeSetCountryCode", "([B)B", (void *)uwbNativeManager_SetCountryCode},
+    {"nativeSendRawVendorCmd", "(II[B)Lcom/android/server/uwb/data/UwbVendorUciResponse;",
+    (void*)uwbNativeManager_sendRawUci},
+    {"nativeEnableConformanceTest", "(Z)B",
+     (void*)uwbNativeManager_enableConformanceTest},
+    {"nativeGetMaxSessionNumber", "()I",
+     (void *)uwbNativeManager_getMaxSessionNumber},
+    {"nativeResetDevice", "(B)B", (void *)uwbNativeManager_resetDevice},
+    {"nativeGetSpecificationInfo",
+     "()Lcom/android/server/uwb/info/UwbSpecificationInfo;",
+     (void *)uwbNativeManager_getSpecificationInfo},
+    {"nativeGetCapsInfo", "()Lcom/android/server/uwb/data/UwbTlvData;",
+     (void*)uwbNativeManager_GetDeviceCapebilityParams}
+};
+
+/*******************************************************************************
+**
+** Function:        register_UwbNativeManager
+**
+** Description:     Regisgter JNI functions of UwbEventManager class with Java
+*Virtual Machine.
+**
+** Params:          env: Environment of JVM.
+**
+** Returns:         Status of registration (JNI version).
+**
+*******************************************************************************/
+int register_com_android_uwb_dhimpl_UwbNativeManager(JNIEnv *env) {
+  static const char fn[] = "register_com_android_uwb_dhimpl_UwbNativeManager";
+  UNUSED(fn);
+  JNI_TRACE_I("%s: enter", fn);
+  return jniRegisterNativeMethods(env, UWB_NATIVE_MANAGER_CLASS_NAME, gMethods,
+                                  sizeof(gMethods) / sizeof(gMethods[0]));
+}
+
+} // namespace android
diff --git a/service/uci/jni/rfTest/UwbRfTestManager.cpp b/service/uci/jni/rfTest/UwbRfTestManager.cpp
new file mode 100755
index 0000000..d526f25
--- /dev/null
+++ b/service/uci/jni/rfTest/UwbRfTestManager.cpp
@@ -0,0 +1,879 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Copyright 2021 NXP.
+ *
+ * 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 "UwbJniInternal.h"
+#include "UwbRfTestManager.h"
+#include "JniLog.h"
+#include "ScopedJniEnv.h"
+#include "SyncEvent.h"
+#include "UwbAdaptation.h"
+#include "uwb_config.h"
+#include "uwb_hal_int.h"
+
+namespace android {
+
+const char *PERIODIC_TX_DATA_CLASS_NAME =
+    "com/android/uwb/test/UwbTestPeriodicTxResult";
+const char *PER_RX_DATA_CLASS_NAME =
+    "com/android/uwb/test/UwbTestRxPacketErrorRateResult";
+const char *UWB_LOOPBACK_DATA_CLASS_NAME =
+    "com/android/uwb/test/UwbTestLoopBackTestResult";
+const char *RX_DATA_CLASS_NAME = "com/android/uwb/test/UwbTestRxResult";
+
+static SyncEvent sUwaRfTestEvent;
+static SyncEvent sUwaSetTestConfigEvent;
+static SyncEvent sUwaGetTestConfigEvent;
+static uint8_t sSetTestConfig[UCI_MAX_PAYLOAD_SIZE];
+static uint8_t sGetTestConfig[UCI_MAX_PAYLOAD_SIZE];
+static uint8_t sNoOfTestConfigIds = 0x00;
+static uint16_t sGetTestConfigLen;
+static uint16_t sSetTestConfigLen;
+static uint8_t sGetTestConfigStatus;
+static uint8_t sSetTestConfigStatus;
+
+/* command response status */
+static bool setTestConfigRespStatus = false;
+static bool getTestConfigRespStatus = false;
+static bool rfTestStatus = false;
+bool IsRfTestOngoing = false; // to track the RF test status whether test is
+                              // sussesffuly completed or not
+
+static UwbRfTestManager &uwbRfTestManager = UwbRfTestManager::getInstance();
+
+void clearRfTestContext() { IsRfTestOngoing = false; }
+
+UwbRfTestManager UwbRfTestManager::mObjTestManager;
+
+UwbRfTestManager &UwbRfTestManager::getInstance() { return mObjTestManager; }
+
+UwbRfTestManager::UwbRfTestManager() {
+  mVm = NULL;
+  mClass = NULL;
+  mObject = NULL;
+  ;
+  mPeriodicTxDataClass = NULL;
+  mPerRxDataClass = NULL;
+  mUwbLoopBackDataClass = NULL;
+  mRxDataClass = NULL;
+  mOnPeriodicTxDataNotificationReceived = NULL;
+  mOnPerRxDataNotificationReceived = NULL;
+  mOnLoopBackTestDataNotificationReceived = NULL;
+  mOnRxTestDataNotificationReceived = NULL;
+}
+
+void UwbRfTestManager::onPeriodicTxDataNotificationReceived(uint16_t len,
+                                                            uint8_t *data) {
+  ScopedJniEnv env(mVm);
+  if (env == NULL) {
+    JNI_TRACE_E("%s: jni env is null", __func__);
+    return;
+  }
+
+  tPERIODIC_TX_DATA sPeriodic_tx_data;
+  memset(&sPeriodic_tx_data, 0, sizeof(tPERIODIC_TX_DATA));
+  if (len != 0) {
+    STREAM_TO_UINT8(sPeriodic_tx_data.status, data);
+
+    jmethodID periodicTxCtor =
+        env->GetMethodID(mPeriodicTxDataClass, "<init>", "(I)V");
+    jobject periodicTxObject = env->NewObject(
+        mPeriodicTxDataClass, periodicTxCtor, (int)sPeriodic_tx_data.status);
+
+    if (mOnPeriodicTxDataNotificationReceived != NULL) {
+      env->CallVoidMethod(mObject, mOnPeriodicTxDataNotificationReceived,
+                          periodicTxObject);
+      if (env->ExceptionCheck()) {
+        env->ExceptionClear();
+        JNI_TRACE_E("%s: fail to send periodic TX test status", __func__);
+      }
+    } else {
+      JNI_TRACE_E("%s: periodic TX data MID is NULL", __func__);
+    }
+  }
+}
+
+void UwbRfTestManager::onPerRxDataNotificationReceived(uint16_t len,
+                                                       uint8_t *data) {
+  ScopedJniEnv env(mVm);
+  if (env == NULL) {
+    JNI_TRACE_E("%s: jni env is null", __func__);
+    return;
+  }
+
+  tPER_RX_DATA sPer_rx_data;
+
+  memset(&sPer_rx_data, 0, sizeof(tPER_RX_DATA));
+  if (len != 0) {
+    STREAM_TO_UINT8(sPer_rx_data.status, data);
+    STREAM_TO_UINT32(sPer_rx_data.attempts, data);
+    STREAM_TO_UINT32(sPer_rx_data.ACQ_detect, data);
+    STREAM_TO_UINT32(sPer_rx_data.ACQ_rejects, data);
+    STREAM_TO_UINT32(sPer_rx_data.RX_fail, data);
+    STREAM_TO_UINT32(sPer_rx_data.sync_cir_ready, data);
+    STREAM_TO_UINT32(sPer_rx_data.sfd_fail, data);
+    STREAM_TO_UINT32(sPer_rx_data.sfd_found, data);
+    STREAM_TO_UINT32(sPer_rx_data.phr_dec_error, data);
+    STREAM_TO_UINT32(sPer_rx_data.phr_bit_error, data);
+    STREAM_TO_UINT32(sPer_rx_data.psdu_dec_error, data);
+    STREAM_TO_UINT32(sPer_rx_data.psdu_bit_error, data);
+    STREAM_TO_UINT32(sPer_rx_data.sts_found, data);
+    STREAM_TO_UINT32(sPer_rx_data.eof, data);
+
+    jmethodID perRxCtor =
+        env->GetMethodID(mPerRxDataClass, "<init>", "(IJJJJJJJJJJJJJ)V");
+    jobject perRxObject = env->NewObject(
+        mPerRxDataClass, perRxCtor, (int)sPer_rx_data.status,
+        (long)sPer_rx_data.attempts, (long)sPer_rx_data.ACQ_detect,
+        (long)sPer_rx_data.ACQ_rejects, (long)sPer_rx_data.RX_fail,
+        (long)sPer_rx_data.sync_cir_ready, (long)sPer_rx_data.sfd_fail,
+        (long)sPer_rx_data.sfd_found, (long)sPer_rx_data.phr_dec_error,
+        (long)sPer_rx_data.phr_bit_error, (long)sPer_rx_data.psdu_dec_error,
+        (long)sPer_rx_data.psdu_bit_error, (long)sPer_rx_data.sts_found,
+        (long)sPer_rx_data.eof);
+    if (mOnPerRxDataNotificationReceived != NULL) {
+      env->CallVoidMethod(mObject, mOnPerRxDataNotificationReceived,
+                          perRxObject);
+      if (env->ExceptionCheck()) {
+        env->ExceptionClear();
+        JNI_TRACE_E("%s: fail to send PER Rx test data", __func__);
+      }
+    } else {
+      JNI_TRACE_E("%s: PER Rx data MID is NULL", __func__);
+    }
+  }
+}
+
+void UwbRfTestManager::onLoopBackTestDataNotificationReceived(uint16_t len,
+                                                              uint8_t *data) {
+  ScopedJniEnv env(mVm);
+  if (env == NULL) {
+    JNI_TRACE_E("%s: jni env is null", __func__);
+    return;
+  }
+
+  tUWB_LOOPBACK_DATA sUwb_loopback_data;
+  jbyteArray psduData = NULL;
+  uint16_t psduDataLen = 0;
+  memset(&sUwb_loopback_data, 0, sizeof(tUWB_LOOPBACK_DATA));
+  if (len != 0) {
+    STREAM_TO_UINT8(sUwb_loopback_data.status, data);
+    STREAM_TO_UINT32(sUwb_loopback_data.txts_int, data);
+    STREAM_TO_UINT16(sUwb_loopback_data.txts_frac, data);
+    STREAM_TO_UINT32(sUwb_loopback_data.rxts_int, data);
+    STREAM_TO_UINT16(sUwb_loopback_data.rxts_frac, data);
+    STREAM_TO_UINT16(sUwb_loopback_data.aoa_azimuth, data);
+    STREAM_TO_UINT16(sUwb_loopback_data.aoa_elevation, data);
+    STREAM_TO_UINT16(sUwb_loopback_data.phr, data);
+    STREAM_TO_UINT16(sUwb_loopback_data.psdu_data_length, data);
+    psduDataLen = sUwb_loopback_data.psdu_data_length;
+    if (sUwb_loopback_data.psdu_data_length > 0) {
+      STREAM_TO_ARRAY(sUwb_loopback_data.psdu_data, data,
+                      sUwb_loopback_data.psdu_data_length);
+      psduData = env->NewByteArray(sUwb_loopback_data.psdu_data_length);
+      env->SetByteArrayRegion(psduData, 0, sUwb_loopback_data.psdu_data_length,
+                              (jbyte *)sUwb_loopback_data.psdu_data);
+    }
+
+    jmethodID uwbLoopBackCtor =
+        env->GetMethodID(mUwbLoopBackDataClass, "<init>", "(IJIJIIII[B)V");
+    jobject uwbLoopBackObject = env->NewObject(
+        mUwbLoopBackDataClass, uwbLoopBackCtor, (int)sUwb_loopback_data.status,
+        (long)sUwb_loopback_data.txts_int, (int)sUwb_loopback_data.txts_frac,
+        (long)sUwb_loopback_data.rxts_int, (int)sUwb_loopback_data.rxts_frac,
+        (int)sUwb_loopback_data.aoa_azimuth,
+        (int)sUwb_loopback_data.aoa_elevation, (int)sUwb_loopback_data.phr,
+        psduData);
+    if (mOnLoopBackTestDataNotificationReceived != NULL) {
+      env->CallVoidMethod(mObject, mOnLoopBackTestDataNotificationReceived,
+                          uwbLoopBackObject);
+      if (env->ExceptionCheck()) {
+        env->ExceptionClear();
+        JNI_TRACE_E("%s: fail to send rf loopback test data", __func__);
+      }
+    } else {
+      JNI_TRACE_E("%s: rf loopback data MID is NULL", __func__);
+    }
+  }
+}
+
+void UwbRfTestManager::onRxTestDataNotificationReceived(uint16_t len,
+                                                        uint8_t *data) {
+  ScopedJniEnv env(mVm);
+  if (env == NULL) {
+    JNI_TRACE_E("%s: jni env is null", __func__);
+    return;
+  }
+
+  tUWB_RX_DATA sRx_data;
+  jbyteArray psduData = NULL;
+  uint16_t psduDataLen = 0;
+  uint16_t aoaFirst, aoaSecond;
+  memset(&sRx_data, 0, sizeof(tUWB_RX_DATA));
+  if (len != 0) {
+    STREAM_TO_UINT8(sRx_data.status, data);
+    STREAM_TO_UINT32(sRx_data.rx_done_ts_int, data);
+    STREAM_TO_UINT16(sRx_data.rx_done_ts_frac, data);
+    /* Extract First AoA (first 2 bytes) */
+    STREAM_TO_UINT16(aoaFirst, data);
+    /* Extract Second AoA (next 2 bytes) */
+    STREAM_TO_UINT16(aoaSecond, data);
+    STREAM_TO_UINT8(sRx_data.toa_gap, data);
+    STREAM_TO_UINT16(sRx_data.phr, data);
+    STREAM_TO_UINT16(sRx_data.psdu_data_length, data);
+    psduDataLen = sRx_data.psdu_data_length;
+    if (sRx_data.psdu_data_length > 0) {
+      STREAM_TO_ARRAY(sRx_data.psdu_data, data, sRx_data.psdu_data_length);
+      psduData = env->NewByteArray(sRx_data.psdu_data_length);
+      env->SetByteArrayRegion(psduData, 0, sRx_data.psdu_data_length,
+                              (jbyte *)sRx_data.psdu_data);
+    }
+
+    jmethodID rxDataCtor =
+        env->GetMethodID(mRxDataClass, "<init>", "(IJIIIII[B)V");
+    jobject rxDataObject = env->NewObject(
+        mRxDataClass, rxDataCtor, (int)sRx_data.status,
+        (long)sRx_data.rx_done_ts_int, (int)sRx_data.rx_done_ts_frac,
+        (int)aoaFirst, (int)aoaSecond, (int)sRx_data.toa_gap, (int)sRx_data.phr,
+        psduData);
+    if (mOnRxTestDataNotificationReceived != NULL) {
+      env->CallVoidMethod(mObject, mOnRxTestDataNotificationReceived,
+                          rxDataObject);
+      if (env->ExceptionCheck()) {
+        env->ExceptionClear();
+        JNI_TRACE_E("%s: fail to send Rx test data", __func__);
+      }
+    } else {
+      JNI_TRACE_E("%s: Rx test data MID is NULL", __func__);
+    }
+  }
+}
+
+void UwbRfTestManager::doLoadSymbols(JNIEnv *env, jobject thiz) {
+  JNI_TRACE_I("%s: enter", __func__);
+  env->GetJavaVM(&mVm);
+
+  jclass clazz = env->GetObjectClass(thiz);
+  if (clazz != NULL) {
+    mClass = (jclass)env->NewGlobalRef(clazz);
+    // The reference is only used as a proxy for callbacks.
+    mObject = env->NewGlobalRef(thiz);
+    mOnPeriodicTxDataNotificationReceived =
+        env->GetMethodID(clazz, "onPeriodicTxDataNotificationReceived",
+                         "(Lcom/android/uwb/test/UwbTestPeriodicTxResult;)V");
+    mOnPerRxDataNotificationReceived = env->GetMethodID(
+        clazz, "onPerRxDataNotificationReceived",
+        "(Lcom/android/uwb/test/UwbTestRxPacketErrorRateResult;)V");
+    mOnLoopBackTestDataNotificationReceived =
+        env->GetMethodID(clazz, "onLoopBackTestDataNotificationReceived",
+                         "(Lcom/android/uwb/test/UwbTestLoopBackTestResult;)V");
+    mOnRxTestDataNotificationReceived =
+        env->GetMethodID(clazz, "onRxTestDataNotificationReceived",
+                         "(Lcom/android/uwb/test/UwbTestRxResult;)V");
+
+    uwb_jni_cache_jclass(env, PERIODIC_TX_DATA_CLASS_NAME,
+                         &mPeriodicTxDataClass);
+    uwb_jni_cache_jclass(env, PER_RX_DATA_CLASS_NAME, &mPerRxDataClass);
+    uwb_jni_cache_jclass(env, UWB_LOOPBACK_DATA_CLASS_NAME,
+                         &mUwbLoopBackDataClass);
+    uwb_jni_cache_jclass(env, RX_DATA_CLASS_NAME, &mRxDataClass);
+  }
+  JNI_TRACE_I("%s: exit", __func__);
+}
+
+/*******************************************************************************
+**
+** Function:        setTestConfigurations()
+**
+** Description:     application shall configure the Test configuration
+*parameters
+**
+** Params:          env: JVM environment.
+**                     o: Java object.
+**                     sessionId: All Test configurations belonging to this
+*Session ID
+**                     noOfParams : The number of Test Configuration fields to
+*follow
+**                     testConfigLen : Length of TestConfigData
+**                     TestConfig : Test Configurations for session
+**
+** Returns:         Returns byte array
+**
+**
+*******************************************************************************/
+jbyteArray UwbRfTestManager::setTestConfigurations(JNIEnv *env, jobject o,
+                                                   jint sessionId,
+                                                   jint noOfParams,
+                                                   jint testConfigLen,
+                                                   jbyteArray TestConfig) {
+  tUWA_STATUS status;
+  jbyteArray testConfigArray = NULL;
+  uint8_t *testConfigData = NULL;
+  JNI_TRACE_I("%s: Enter", __func__);
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", __func__);
+    return testConfigArray;
+  }
+
+  testConfigData = (uint8_t *)malloc(sizeof(uint8_t) * testConfigLen);
+  if (testConfigData != NULL) {
+    memset(testConfigData, 0, (sizeof(uint8_t) * testConfigLen));
+    env->GetByteArrayRegion(TestConfig, 0, testConfigLen,
+                            (jbyte *)testConfigData);
+    setTestConfigRespStatus = false;
+    SyncEventGuard guard(sUwaSetTestConfigEvent);
+    JNI_TRACE_I("%d: testConfigLen", testConfigLen);
+    status =
+        UWA_TestSetConfig(sessionId, noOfParams, testConfigLen, testConfigData);
+    free(testConfigData);
+    if (status == UWA_STATUS_OK) {
+      sUwaSetTestConfigEvent.wait(UWB_CMD_TIMEOUT);
+      JNI_TRACE_E("%s: Success UWA_TestSetConfig Command", __func__);
+      if (setTestConfigRespStatus) {
+        testConfigArray =
+            env->NewByteArray(sSetTestConfigLen + sizeof(sSetTestConfigStatus) +
+                              sizeof(sNoOfTestConfigIds));
+        env->SetByteArrayRegion(testConfigArray, 0, 1,
+                                (jbyte *)&sSetTestConfigStatus);
+        env->SetByteArrayRegion(testConfigArray, 1, 1,
+                                (jbyte *)&sNoOfTestConfigIds);
+        if (sSetTestConfigLen > 0) {
+          env->SetByteArrayRegion(testConfigArray, 2, sSetTestConfigLen,
+                                  (jbyte *)&sSetTestConfig[0]);
+        }
+      }
+    } else {
+      JNI_TRACE_E("%s: Failed UWA_TestSetConfig", __func__);
+      return testConfigArray;
+    }
+  } else {
+    JNI_TRACE_E("%s: Unable to Allocate Memory", __func__);
+  }
+  JNI_TRACE_I("%s: Exit", __func__);
+  return testConfigArray;
+}
+
+/*******************************************************************************
+**
+** Function:        getTestConfigurations
+**
+** Description:     application shall retrieve the Test configuration parameters
+**
+** Params:         env: JVM environment.
+**                     o: Java object.
+**                     session id : Session Id to which get All test Config list
+**                     noOfParams: Number of Test Config Params
+**                     testConfigLen: Total Test Config lentgh
+**                     TestConfig: Test Config Id List
+**
+** Returns:          Returns byte array
+**
+*******************************************************************************/
+jbyteArray UwbRfTestManager::getTestConfigurations(JNIEnv *env, jobject o,
+                                                   jint sessionId,
+                                                   jint noOfParams,
+                                                   jint testConfigLen,
+                                                   jbyteArray TestConfig) {
+  tUWA_STATUS status;
+  jbyteArray testConfigArray = NULL;
+  uint8_t *testConfigData = NULL;
+  JNI_TRACE_I("%s: Enter", __func__);
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", __func__);
+    return testConfigArray;
+  }
+
+  getTestConfigRespStatus = false;
+  testConfigData = (uint8_t *)malloc(sizeof(uint8_t) * testConfigLen);
+  if (testConfigData != NULL) {
+    memset(testConfigData, 0, (sizeof(uint8_t) * testConfigLen));
+    env->GetByteArrayRegion(TestConfig, 0, testConfigLen,
+                            (jbyte *)testConfigData);
+    SyncEventGuard guard(sUwaGetTestConfigEvent);
+    status =
+        UWA_TestGetConfig(sessionId, noOfParams, testConfigLen, testConfigData);
+    if (status == UWA_STATUS_OK) {
+      sUwaGetTestConfigEvent.wait(UWB_CMD_TIMEOUT);
+      if (getTestConfigRespStatus) {
+        testConfigArray =
+            env->NewByteArray(sGetTestConfigLen + sizeof(sNoOfTestConfigIds) +
+                              sizeof(sGetTestConfigStatus));
+        env->SetByteArrayRegion(testConfigArray, 0, 1,
+                                (jbyte *)&sGetTestConfigStatus);
+        env->SetByteArrayRegion(testConfigArray, 1, 1,
+                                (jbyte *)&sNoOfTestConfigIds);
+        env->SetByteArrayRegion(testConfigArray, 2, sGetTestConfigLen,
+                                (jbyte *)&sGetTestConfig[0]);
+      }
+    } else {
+      JNI_TRACE_E("%s: Failed UWA_TestGetConfig", __func__);
+    }
+    free(testConfigData);
+  } else {
+    JNI_TRACE_E("%s: Unable to Allocate Memory", __func__);
+  }
+  JNI_TRACE_I("%s: Exit", __func__);
+  return testConfigArray;
+}
+
+/*******************************************************************************
+**
+** Function:        startPerRxTest
+**
+** Description:     start PER RX performance test
+**
+** Params:          env: JVM environment.
+**                     o: Java object.
+**                     refPsduData : Reference Psdu Data
+**
+** Returns:         UWA_STATUS_OK if success else returns
+**                     UWA_STATUS_FAILED
+*******************************************************************************/
+jbyte UwbRfTestManager::startPerRxTest(JNIEnv *env, jobject o,
+                                       jbyteArray refPsduData) {
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+  uint16_t dataLen = 0;
+  uint8_t *ref_psdu_data = NULL;
+  JNI_TRACE_I("%s: Enter; ", __func__);
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", __func__);
+    return status;
+  }
+
+  if (IsRfTestOngoing) {
+    JNI_TRACE_E("%s: UWB device Rf Test is Ongoing already", __func__);
+    return status;
+  }
+
+  rfTestStatus = false;
+  if (refPsduData != NULL) {
+    dataLen = env->GetArrayLength(refPsduData);
+    if (dataLen > 0) {
+      ref_psdu_data = (uint8_t *)malloc(sizeof(uint8_t) * dataLen);
+      if (ref_psdu_data != NULL) {
+        memset(ref_psdu_data, 0, (sizeof(uint8_t) * dataLen));
+        env->GetByteArrayRegion(refPsduData, 0, dataLen,
+                                (jbyte *)ref_psdu_data);
+
+        SyncEventGuard guard(sUwaRfTestEvent);
+        IsRfTestOngoing = true;
+        status = UWA_PerRxTest(dataLen, ref_psdu_data);
+        if (UWA_STATUS_OK == status) {
+          sUwaRfTestEvent.wait(UWB_CMD_TIMEOUT);
+          if (!rfTestStatus) {
+            IsRfTestOngoing = false;
+          }
+        } else {
+          IsRfTestOngoing = false;
+          JNI_TRACE_E("%s: UWA_PerRxTest Failed", __func__);
+        }
+        free(ref_psdu_data);
+      } else {
+        JNI_TRACE_E("%s: Unable to Allocate Memory", __func__);
+      }
+    } else {
+      JNI_TRACE_I("%s: Length of refPsduData array is 0; ", __func__);
+    }
+  }
+  JNI_TRACE_I("%s: Exit", __func__);
+  return (rfTestStatus) ? UWA_STATUS_OK : UWA_STATUS_FAILED;
+}
+
+/*******************************************************************************
+**
+** Function:        startPeriodicTxTest
+**
+** Description:     start PERIODIC Tx Test
+**
+** Params:         env: JVM environment.
+**                    o: Java object.
+**                    psduData : Reference Psdu Data
+**
+** Returns:         UWb_STATUS_OK if success  else returns
+**                     UWA_STATUS_FAILED
+**
+*******************************************************************************/
+jbyte UwbRfTestManager::startPeriodicTxTest(JNIEnv *env, jobject o,
+                                            jbyteArray psduData) {
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+  uint16_t dataLen = 0;
+  uint8_t *psdu_Data = NULL;
+  JNI_TRACE_I("%s: Enter; ", __func__);
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", __func__);
+    return status;
+  }
+
+  if (IsRfTestOngoing) {
+    JNI_TRACE_E("%s: UWB device Rf Test is Ongoing already", __func__);
+    return status;
+  }
+
+  rfTestStatus = false;
+  if (psduData != NULL) {
+    dataLen = env->GetArrayLength(psduData);
+    if (dataLen > UCI_MAX_PAYLOAD_SIZE) {
+      JNI_TRACE_E("%s: PER TX data size exceeds %d", __func__,
+                  UCI_MAX_PAYLOAD_SIZE);
+      return status;
+    }
+    psdu_Data = (uint8_t *)malloc(sizeof(uint8_t) * dataLen);
+    if (psdu_Data != NULL) {
+      memset(psdu_Data, 0, (sizeof(uint8_t) * dataLen));
+      env->GetByteArrayRegion(psduData, 0, dataLen, (jbyte *)psdu_Data);
+
+      SyncEventGuard guard(sUwaRfTestEvent);
+      IsRfTestOngoing = true;
+      status = UWA_PeriodicTxTest(dataLen, psdu_Data);
+      if (UWA_STATUS_OK == status) {
+        sUwaRfTestEvent.wait(UWB_CMD_TIMEOUT);
+        if (!rfTestStatus) {
+          IsRfTestOngoing = false;
+        }
+      } else {
+        IsRfTestOngoing = false;
+        JNI_TRACE_E("%s: UWA_PeriodicTxTest Failed", __func__);
+      }
+      free(psdu_Data);
+    } else {
+      JNI_TRACE_E("%s: Unable to Allocate Memory", __func__);
+    }
+  }
+
+  JNI_TRACE_I("%s: Exit", __func__);
+  return (rfTestStatus) ? UWA_STATUS_OK : UWA_STATUS_FAILED;
+}
+
+/*******************************************************************************
+**
+** Function:        startUwbLoopBackTest
+**
+** Description:     start Rf Loop back test
+**
+** Params:          env: JVM environment.
+**                     o: Java object.
+**                     psduData : Reference Psdu Data
+**
+** Returns:         UWA_STATUS_OK if success else returns
+**                     UWA_STATUS_FAILED
+**
+*******************************************************************************/
+jbyte UwbRfTestManager::startUwbLoopBackTest(JNIEnv *env, jobject o,
+                                             jbyteArray psduData) {
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+  uint16_t dataLen = 0;
+  uint8_t *psdu_Data = NULL;
+  JNI_TRACE_I("%s: Enter; ", __func__);
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", __func__);
+    return status;
+  }
+
+  if (IsRfTestOngoing) {
+    JNI_TRACE_I("%s: UWB device Rf Test is Ongoing already", __func__);
+    return status;
+  }
+
+  rfTestStatus = false;
+  if (psduData != NULL) {
+    dataLen = env->GetArrayLength(psduData);
+    if (dataLen > UCI_MAX_PAYLOAD_SIZE) {
+      JNI_TRACE_E("%s: Loopback data size exceeds %d", __func__,
+                  UCI_MAX_PAYLOAD_SIZE);
+      return UWA_STATUS_FAILED;
+    }
+    psdu_Data = (uint8_t *)malloc(sizeof(uint8_t) * dataLen);
+    if (psdu_Data != NULL) {
+      memset(psdu_Data, 0, (sizeof(uint8_t) * dataLen));
+      env->GetByteArrayRegion(psduData, 0, dataLen, (jbyte *)psdu_Data);
+
+      SyncEventGuard guard(sUwaRfTestEvent);
+      IsRfTestOngoing = true;
+      status = UWA_UwbLoopBackTest(dataLen, psdu_Data);
+      if (UWA_STATUS_OK == status) {
+        sUwaRfTestEvent.wait(UWB_CMD_TIMEOUT);
+        if (!rfTestStatus) {
+          IsRfTestOngoing = false;
+        }
+      } else {
+        IsRfTestOngoing = false;
+        JNI_TRACE_E("%s: UWA_UwbLoopBackTest failed", __func__);
+      }
+      free(psdu_Data);
+    } else {
+      JNI_TRACE_E("%s: Unable to Allocate Memory", __func__);
+    }
+  }
+
+  JNI_TRACE_I("%s: Exit", __func__);
+  return (rfTestStatus) ? UWA_STATUS_OK : UWA_STATUS_FAILED;
+}
+
+/*******************************************************************************
+**
+** Function:        startRxTest
+**
+** Description:     start Rx test
+**
+** Params:          env: JVM environment.
+**                      o: Java object.
+**
+** Returns:         UWA_STATUS_OK if success  else returns
+**                     UWA_STATUS_FAILED
+**
+*******************************************************************************/
+jbyte UwbRfTestManager::startRxTest(JNIEnv *env, jobject o) {
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+  JNI_TRACE_I("%s: Enter; ", __func__);
+
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", __func__);
+    return status;
+  }
+
+  if (IsRfTestOngoing) {
+    JNI_TRACE_I("%s: UWB device Rf Test is Ongoing already", __func__);
+    return status;
+  }
+
+  rfTestStatus = false;
+  SyncEventGuard guard(sUwaRfTestEvent);
+  IsRfTestOngoing = true;
+  status = UWA_RxTest();
+  if (UWA_STATUS_OK == status) {
+    sUwaRfTestEvent.wait(UWB_CMD_TIMEOUT);
+    if (!rfTestStatus) {
+      IsRfTestOngoing = false;
+    }
+  } else {
+    IsRfTestOngoing = false;
+    JNI_TRACE_E("%s: UWA_RxTest failed", __func__);
+  }
+
+  JNI_TRACE_I("%s: Exit", __func__);
+  return (rfTestStatus) ? UWA_STATUS_OK : UWA_STATUS_FAILED;
+}
+
+/*******************************************************************************
+**
+** Function:        stopRfTest
+**
+** Description:     stop PER performance test
+**
+** Params:          env: JVM environment.
+**                      o: Java object.
+**
+** Returns:         UWA_STATUS_OK if success  else returns
+**                     UWA_STATUS_FAILED
+*******************************************************************************/
+jbyte UwbRfTestManager::stopRfTest(JNIEnv *env, jobject o) {
+  tUWA_STATUS status = UWA_STATUS_FAILED;
+  JNI_TRACE_I("%s: Enter; ", __func__);
+  if (!gIsUwaEnabled) {
+    JNI_TRACE_E("%s: UWB device is not initialized", __func__);
+    return status;
+  }
+
+  rfTestStatus = false;
+  SyncEventGuard guard(sUwaRfTestEvent);
+  status = UWA_TestStopSession();
+  if (UWA_STATUS_OK == status) {
+    sUwaRfTestEvent.wait(UWB_CMD_TIMEOUT);
+  } else {
+    JNI_TRACE_E("%s: UWA_TestStopSession failed", __func__);
+  }
+
+  if (rfTestStatus) {
+    IsRfTestOngoing = false;
+  }
+  JNI_TRACE_I("%s: Exit", __func__);
+  return (rfTestStatus) ? UWA_STATUS_OK : UWA_STATUS_FAILED;
+}
+
+/*******************************************************************************
+**
+** Function:        uwaRfTestDeviceManagementCallback
+**
+** Description:     Receive Rf Test related device management events from UCI
+*stack
+**                  dmEvent: Device-management event ID.
+**                  eventData: Data associated with event ID.
+**
+** Returns:         None
+**
+*******************************************************************************/
+void uwaRfTestDeviceManagementCallback(uint8_t dmEvent,
+                                       tUWA_DM_TEST_CBACK_DATA *eventData) {
+  JNI_TRACE_I("%s: enter; event=0x%X", __func__, dmEvent);
+
+  switch (dmEvent) {
+  case UWA_DM_TEST_SET_CONFIG_RSP_EVT: // result of UWA_TestSetConfig
+    JNI_TRACE_I("%s: UWA_DM_TEST_SET_CONFIG_RSP_EVT", __func__);
+    {
+      SyncEventGuard guard(sUwaSetTestConfigEvent);
+      setTestConfigRespStatus = true;
+      sSetTestConfigStatus = eventData->status;
+      sSetTestConfigLen = eventData->sTest_set_config.tlv_size;
+      sNoOfTestConfigIds = eventData->sTest_set_config.num_param_id;
+      if (eventData->sTest_set_config.tlv_size > 0) {
+        memcpy(sSetTestConfig, eventData->sTest_set_config.param_ids,
+               eventData->sTest_set_config.tlv_size);
+      }
+      sUwaSetTestConfigEvent.notifyOne();
+    }
+    break;
+  case UWA_DM_TEST_GET_CONFIG_RSP_EVT: /* Result of UWA_TestGetConfig */
+    JNI_TRACE_I("%s: UWA_DM_TEST_GET_CONFIG_RSP_EVT", __func__);
+    {
+      SyncEventGuard guard(sUwaGetTestConfigEvent);
+      getTestConfigRespStatus = true;
+      sGetTestConfigStatus = eventData->status;
+      sGetTestConfigLen = eventData->sTest_get_config.tlv_size;
+      sNoOfTestConfigIds = eventData->sTest_get_config.no_of_ids;
+      if (eventData->sTest_get_config.tlv_size) {
+        memcpy(sGetTestConfig, eventData->sTest_get_config.param_tlvs,
+               eventData->sTest_get_config.tlv_size);
+      }
+      sUwaGetTestConfigEvent.notifyOne();
+    }
+    break;
+
+  case UWA_DM_TEST_PERIODIC_TX_RSP_EVT: /* result of periodic tx command */
+    JNI_TRACE_I("%s: UWA_DM_TEST_PERIODIC_TX_RSP_EVT", __func__);
+    {
+      SyncEventGuard guard(sUwaRfTestEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        rfTestStatus = true;
+        JNI_TRACE_I("%s: UWA_DM_TEST_PERIODIC_TX_RSP_EVT Success", __func__);
+      } else {
+        JNI_TRACE_E("%s: UWA_DM_TEST_PERIODIC_TX_RSP_EVT failed", __func__);
+      }
+      sUwaRfTestEvent.notifyOne();
+    }
+    break;
+
+  case UWA_DM_TEST_PER_RX_RSP_EVT: /* result of per rx command */
+    JNI_TRACE_I("%s: UWA_DM_TEST_PER_RX_RSP_EVT", __func__);
+    {
+      SyncEventGuard guard(sUwaRfTestEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        rfTestStatus = true;
+        JNI_TRACE_I("%s: UWA_DM_TEST_PER_RX_RSP_EVT Success", __func__);
+      } else {
+        JNI_TRACE_E("%s: UWA_DM_TEST_PER_RX_RSP_EVT failed", __func__);
+      }
+      sUwaRfTestEvent.notifyOne();
+    }
+    break;
+
+  case UWA_DM_TEST_LOOPBACK_RSP_EVT: /* result of rf loop back command */
+    JNI_TRACE_I("%s: UWA_DM_TEST_UWB_LOOPBACK_EVT", __func__);
+    {
+      SyncEventGuard guard(sUwaRfTestEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        rfTestStatus = true;
+        JNI_TRACE_I("%s: UWA_DM_TEST_UWB_LOOPBACK_EVT Success", __func__);
+      } else {
+        JNI_TRACE_E("%s: UWA_DM_TEST_UWB_LOOPBACK_EVT failed", __func__);
+      }
+      sUwaRfTestEvent.notifyOne();
+    }
+    break;
+
+  case UWA_DM_TEST_RX_RSP_EVT: /* result of rx test command */
+    JNI_TRACE_I("%s: UWA_DM_TEST_RX_RSP_EVT", __func__);
+    {
+      SyncEventGuard guard(sUwaRfTestEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        rfTestStatus = true;
+        JNI_TRACE_I("%s: UWA_DM_TEST_RX_RSP_EVT Success", __func__);
+      } else {
+        JNI_TRACE_E("%s: UWA_DM_TEST_RX_RSP_EVT failed", __func__);
+      }
+      sUwaRfTestEvent.notifyOne();
+    }
+    break;
+
+  case UWA_DM_TEST_STOP_SESSION_RSP_EVT: /* result of per stop command */
+    JNI_TRACE_I("%s: UWA_DM_TEST_STOP_SESSION_RSP_EVT", __func__);
+    {
+      SyncEventGuard guard(sUwaRfTestEvent);
+      if (eventData->status == UWA_STATUS_OK) {
+        rfTestStatus = true;
+        JNI_TRACE_I("%s: UWA_DM_TEST_STOP_SESSION_RSP_EVT Success", __func__);
+      } else {
+        JNI_TRACE_E("%s: UWA_DM_TEST_STOP_SESSION_RSP_EVT failed", __func__);
+      }
+      sUwaRfTestEvent.notifyOne();
+    }
+    break;
+
+  case UWA_DM_TEST_PERIODIC_TX_NTF_EVT:
+    JNI_TRACE_I("%s: UWA_DM_TEST_PERIODIC_TX_NTF_EVT", __func__);
+    {
+      IsRfTestOngoing = false;
+      if (eventData->rf_test_data.length > 0) {
+        uwbRfTestManager.onPeriodicTxDataNotificationReceived(
+            eventData->rf_test_data.length, &eventData->rf_test_data.data[0]);
+      }
+    }
+    break;
+
+  case UWA_DM_TEST_PER_RX_NTF_EVT:
+    JNI_TRACE_I("%s: UWA_DM_TEST_PER_RX_NTF_EVT", __func__);
+    {
+      IsRfTestOngoing = false;
+      if (eventData->rf_test_data.length > 0) {
+        uwbRfTestManager.onPerRxDataNotificationReceived(
+            eventData->rf_test_data.length, &eventData->rf_test_data.data[0]);
+      }
+    }
+    break;
+
+  case UWA_DM_TEST_LOOPBACK_NTF_EVT:
+    JNI_TRACE_I("%s: UWA_DM_TEST_LOOPBACK_NTF_EVT", __func__);
+    {
+      IsRfTestOngoing = false;
+      if (eventData->rf_test_data.length > 0) {
+        uwbRfTestManager.onLoopBackTestDataNotificationReceived(
+            eventData->rf_test_data.length, &eventData->rf_test_data.data[0]);
+      }
+    }
+    break;
+
+  case UWA_DM_TEST_RX_NTF_EVT:
+    JNI_TRACE_I("%s: UWA_DM_TEST_RX_NTF_EVT", __func__);
+    {
+      IsRfTestOngoing = false;
+      if (eventData->rf_test_data.length > 0) {
+        uwbRfTestManager.onRxTestDataNotificationReceived(
+            eventData->rf_test_data.length, &eventData->rf_test_data.data[0]);
+      }
+    }
+    break;
+
+  default:
+    JNI_TRACE_I("%s: unhandled event", __func__);
+    break;
+  }
+}
+} // namespace android
diff --git a/service/uci/jni/rfTest/UwbRfTestManager.h b/service/uci/jni/rfTest/UwbRfTestManager.h
new file mode 100755
index 0000000..1d9ed43
--- /dev/null
+++ b/service/uci/jni/rfTest/UwbRfTestManager.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Copyright 2021 NXP.
+ *
+ * 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 _UWB_RFTEST_NATIVE_MANAGER_H_
+#define _UWB_RFTEST_NATIVE_MANAGER_H_
+namespace android {
+
+typedef struct {
+  uint8_t status;          ///< Status
+  uint32_t attempts;       ///< No. of RX attempts
+  uint32_t ACQ_detect;     ///< No. of times signal was detected
+  uint32_t ACQ_rejects;    ///< No. of times signal was rejected
+  uint32_t RX_fail;        ///< No. of times RX did not go beyond ACQ stage
+  uint32_t sync_cir_ready; ///< No. of times sync CIR ready event was received
+  uint32_t sfd_fail;  ///< No. of time RX was stuck at either ACQ detect or sync
+                      ///< CIR ready
+  uint32_t sfd_found; ///< No. of times SFD was found
+  uint32_t phr_dec_error;  ///< No. of times PHR decode failed
+  uint32_t phr_bit_error;  ///< No. of times PHR bits in error
+  uint32_t psdu_dec_error; ///< No. of times payload decode failed
+  uint32_t psdu_bit_error; ///< No. of times payload bits in error
+  uint32_t sts_found;      ///< No. of times STS detection was successful
+  uint32_t eof;            ///< No. of times end of frame event was triggered
+} tPER_RX_DATA;
+
+typedef struct {
+  uint8_t status;    //< Status
+  uint32_t txts_int; //< Integer part of timestamp in 1/124.8 us resolution
+  uint16_t
+      txts_frac; //< Fractional part of timestamp in 1/124.8/512 µs resolution
+  uint32_t rxts_int; //< Integer part of timestamp in 1/124.8 us resolution
+  uint16_t
+      rxts_frac; //< Fractional part of timestamp in 1/124.8/512 us resolution
+  uint16_t aoa_azimuth;   //< AoA Azimuth in degrees and it is signed value in
+                          // Q9.7 format
+  uint16_t aoa_elevation; //< AoA Elevation  in degrees and it is signed value
+                          // in Q9.7 format
+  uint16_t phr;           //< Received PHR (bits 0-12 as per IEEE spec)
+  uint16_t psdu_data_length;           //<PSDU Data Length
+  uint8_t psdu_data[UCI_PSDU_SIZE_4K]; ///< Received PSDU Data bytes
+} tUWB_LOOPBACK_DATA;
+
+typedef struct {
+  uint8_t status;           //< Status
+  uint32_t rx_done_ts_int;  //< Integer part of timestamp in 1/124.8MHz ticks
+  uint16_t rx_done_ts_frac; //< Fractional part of timestamp in 1/(128 *
+                            // 499.2MHz) ticks resolution
+  uint16_t aoa_azimuth;     //< AoA Azimuth in degrees and it is signed value in
+                            // Q9.7 format
+  uint16_t aoa_elevation;   //< AoA Elevation  in degrees and it is signed value
+                            // in Q9.7 format
+  uint8_t toa_gap; //< ToA of main path minus ToA of first path in nanoseconds
+  uint16_t phr;    //<Received PHR (bits 0-12 as per IEEE spec)
+  uint16_t psdu_data_length;           //<PSDU Data Length
+  uint8_t psdu_data[UCI_PSDU_SIZE_4K]; //< Received PSDU Data bytes
+} tUWB_RX_DATA;
+
+typedef struct {
+  uint8_t status; ///< Status
+} tPERIODIC_TX_DATA;
+
+class UwbRfTestManager {
+public:
+  static UwbRfTestManager &getInstance();
+  void doLoadSymbols(JNIEnv *env, jobject o);
+
+  /* CallBack functions */
+  void onPeriodicTxDataNotificationReceived(uint16_t len, uint8_t *data);
+  void onPerRxDataNotificationReceived(uint16_t len, uint8_t *data);
+  void onLoopBackTestDataNotificationReceived(uint16_t len, uint8_t *data);
+  void onRxTestDataNotificationReceived(uint16_t len, uint8_t *data);
+
+  /* API functions */
+  jbyteArray setTestConfigurations(JNIEnv *env, jobject o, jint sessionId,
+                                   jint noOfParams, jint testConfigLen,
+                                   jbyteArray TestConfig);
+  jbyteArray getTestConfigurations(JNIEnv *env, jobject o, jint sessionId,
+                                   jint noOfParams, jint testConfigLen,
+                                   jbyteArray TestConfig);
+  jbyte startPerRxTest(JNIEnv *env, jobject o, jbyteArray refPsduData);
+  jbyte startPeriodicTxTest(JNIEnv *env, jobject o, jbyteArray psduData);
+  jbyte startUwbLoopBackTest(JNIEnv *env, jobject o, jbyteArray psduData);
+  jbyte startRxTest(JNIEnv *env, jobject o);
+  jbyte stopRfTest(JNIEnv *env, jobject o);
+
+private:
+  UwbRfTestManager();
+
+  static UwbRfTestManager mObjTestManager;
+
+  JavaVM *mVm;
+
+  jclass mClass;   // Reference to Java  class
+  jobject mObject; // Weak ref to Java object to call on
+
+  jclass mPeriodicTxDataClass;
+  jclass mPerRxDataClass;
+  jclass mUwbLoopBackDataClass;
+  jclass mRxDataClass;
+
+  jmethodID mOnPeriodicTxDataNotificationReceived;
+  jmethodID mOnPerRxDataNotificationReceived;
+  jmethodID mOnLoopBackTestDataNotificationReceived;
+  jmethodID mOnRxTestDataNotificationReceived;
+};
+
+} // namespace android
+#endif
\ No newline at end of file
diff --git a/service/uci/jni/rfTest/UwbRfTestNativeManager.cpp b/service/uci/jni/rfTest/UwbRfTestNativeManager.cpp
new file mode 100755
index 0000000..5d5c852
--- /dev/null
+++ b/service/uci/jni/rfTest/UwbRfTestNativeManager.cpp
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Copyright 2021 NXP.
+ *
+ * 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.
+ */
+
+/* UwbRfTestNativeManager is unused now*/
+/*#include "JniLog.h"
+#include "ScopedJniEnv.h"
+#include "SyncEvent.h"
+#include "UwbAdaptation.h"
+#include "UwbJniInternal.h"
+#include "UwbRfTestManager.h"
+#include "uwb_config.h"
+#include "uwb_hal_int.h"
+
+namespace android {
+
+const char *UWB_RFTEST_NATIVE_MANAGER_CLASS_NAME =
+    "com/android/uwb/jni/NativeUwbRfTestManager";
+
+static UwbRfTestManager &uwbRfTestManager = UwbRfTestManager::getInstance();
+
+*//*******************************************************************************
+**
+** Function:        uwbRfTestNativeManager_setTestConfigurations()
+**
+** Description:     application shall configure the Test configuration
+*parameters
+**
+** Params:          env: JVM environment.
+**                  o: Java object.
+**                  sessionId: All Test configurations belonging to this Session
+*ID
+**                  noOfParams : The number of Test Configuration fields to
+*follow
+**                  testConfigLen : Length of TestConfigData
+**                  TestConfig : Test Configurations for session
+**
+** Returns:         Returns byte array
+**
+**
+*******************************************************************************//*
+jbyteArray uwbRfTestNativeManager_setTestConfigurations(
+    JNIEnv *env, jobject o, jint sessionId, jint noOfParams, jint testConfigLen,
+    jbyteArray testConfigArray) {
+  return uwbRfTestManager.setTestConfigurations(env, o, sessionId, noOfParams,
+                                                testConfigLen, testConfigArray);
+}
+
+*//*******************************************************************************
+**
+** Function:        uwbRfTestNativeManager_getTestConfigurations
+**
+** Description:     application shall retrieve the Test configuration parameters
+**
+** Params:       env: JVM environment.
+**                  o: Java object.
+**                  session id : Session Id to which get All test Config list
+**                  noOfParams: Number of Test Config Params
+**                  testConfigLen: Total Test Config lentgh
+**                  TestConfig: Test Config Id List
+**
+** Returns:         Returns byte array
+**
+*******************************************************************************//*
+jbyteArray uwbRfTestNativeManager_getTestConfigurations(
+    JNIEnv *env, jobject o, jint sessionId, jint noOfParams, jint testConfigLen,
+    jbyteArray testConfigArray) {
+  return uwbRfTestManager.getTestConfigurations(env, o, sessionId, noOfParams,
+                                                testConfigLen, testConfigArray);
+}
+
+*//*******************************************************************************
+**
+** Function:        uwbRfTestNativeManager_startPerRxTest
+**
+** Description:     start Packet Error Rate (PER) RX performance test
+**
+** Params:          env: JVM environment.
+**                      o: Java object.
+**                      refPsduData : Reference Psdu Data
+**
+** Returns:      UWA_STATUS_OK if success  else returns
+**                  UWA_STATUS_FAILED
+*******************************************************************************//*
+jbyte uwbRfTestNativeManager_startPerRxTest(JNIEnv *env, jobject o,
+                                            jbyteArray refPsduData) {
+  return uwbRfTestManager.startPerRxTest(env, o, refPsduData);
+}
+
+*//*******************************************************************************
+**
+** Function:        uwbRfTestNativeManager_startPeriodicTxTest
+**
+** Description:     start PERIODIC Tx Test
+**
+** Params:          env: JVM environment.
+**                      o: Java object.
+**                      psduData : Reference Psdu Data
+**
+** Returns:      UWA_STATUS_OK if success  else returns
+**                  UWA_STATUS_FAILED
+**
+*******************************************************************************//*
+jbyte uwbRfTestNativeManager_startPeriodicTxTest(JNIEnv *env, jobject o,
+                                                 jbyteArray psduData) {
+  return uwbRfTestManager.startPeriodicTxTest(env, o, psduData);
+}
+
+*//*******************************************************************************
+**
+** Function:        uwbRfTestNativeManager_startUwbLoopBackTest
+**
+** Description:     start Rf Loop back test
+**
+** Params:          env: JVM environment.
+**                     o: Java object.
+**                     psduData : Reference Psdu Data
+**
+** Returns:      UWA_STATUS_OK if success  else returns
+**                  UWA_STATUS_FAILED
+**
+*******************************************************************************//*
+jbyte uwbRfTestNativeManager_startUwbLoopBackTest(JNIEnv *env, jobject o,
+                                                  jbyteArray psduData) {
+  return uwbRfTestManager.startUwbLoopBackTest(env, o, psduData);
+}
+
+*//*******************************************************************************
+**
+** Function:        uwbRfTestNativeManager_stopRfTest
+**
+** Description:     stop PER performance test
+**
+** Params:          env: JVM environment.
+**                      o: Java object.
+**
+** Returns:      UWA_STATUS_OK if success  else returns
+**                  UWA_STATUS_FAILED
+*******************************************************************************//*
+jbyte uwbRfTestNativeManager_stopRfTest(JNIEnv *env, jobject o) {
+  return uwbRfTestManager.stopRfTest(env, o);
+}
+
+*//*******************************************************************************
+**
+** Function:        uwbRfTestNativeManager_startRxTest
+**
+** Description:     start RX test
+**
+** Params:       env: JVM environment.
+**                  o: Java object.
+**
+** Returns:      UWA_STATUS_OK if success  else returns
+**                  UWA_STATUS_FAILED
+*******************************************************************************//*
+jbyte uwbRfTestNativeManager_startRxTest(JNIEnv *env, jobject o) {
+  return uwbRfTestManager.startRxTest(env, o);
+}
+
+*//*******************************************************************************
+**
+** Function:        uwbRfTestNativeManager_init
+**
+** Description:     Initialize variables.
+**
+** Params           env: JVM environment.
+**                     o: Java object.
+**
+** Returns:         True if ok.
+**
+*******************************************************************************//*
+jboolean uwbRfTestNativeManager_init(JNIEnv *env, jobject o) {
+  uwbRfTestManager.doLoadSymbols(env, o);
+  return JNI_TRUE;
+}
+
+*//*****************************************************************************
+**
+** JNI functions for android
+** UWB service layer has to invoke these APIs to get required functionality
+**
+*****************************************************************************//*
+static JNINativeMethod gMethods[] = {
+    {"nativeInit", "()Z", (void *)uwbRfTestNativeManager_init},
+    {"nativeSetTestConfigurations", "(III[B)[B",
+     (void *)uwbRfTestNativeManager_setTestConfigurations},
+    {"nativeGetTestConfigurations", "(III[B)[B",
+     (void *)uwbRfTestNativeManager_getTestConfigurations},
+    {"nativeStartPerRxTest", "([B)B",
+     (void *)uwbRfTestNativeManager_startPerRxTest},
+    {"nativeStartPeriodicTxTest", "([B)B",
+     (void *)uwbRfTestNativeManager_startPeriodicTxTest},
+    {"nativeStartUwbLoopBackTest", "([B)B",
+     (void *)uwbRfTestNativeManager_startUwbLoopBackTest},
+    {"nativeStartRxTest", "()B", (void *)uwbRfTestNativeManager_startRxTest},
+    {"nativeStopRfTest", "()B", (void *)uwbRfTestNativeManager_stopRfTest}};
+
+*//*******************************************************************************
+**
+** Function:        register_UwbRfTestNativeManager
+**
+** Description:     Regisgter JNI functions of UwbEventManager class with Java
+*Virtual Machine.
+**
+** Params:          env: Environment of JVM.
+**
+** Returns:         Status of registration (JNI version).
+**
+*******************************************************************************//*
+int register_com_android_uwb_dhimpl_UwbRfTestNativeManager(JNIEnv *env) {
+  JNI_TRACE_I("%s: enter", __func__);
+  return jniRegisterNativeMethods(env, UWB_RFTEST_NATIVE_MANAGER_CLASS_NAME,
+                                  gMethods,
+                                  sizeof(gMethods) / sizeof(gMethods[0]));
+}
+
+} // namespace android*/
diff --git a/service/uci/jni/rust/lib.rs b/service/uci/jni/rust/lib.rs
new file mode 100644
index 0000000..f2d1989
--- /dev/null
+++ b/service/uci/jni/rust/lib.rs
@@ -0,0 +1,1278 @@
+//! jni for uwb native stack
+use jni::objects::{JObject, JValue};
+use jni::sys::{
+    jarray, jboolean, jbyte, jbyteArray, jint, jintArray, jlong, jobject, jshort, jshortArray,
+    jsize,
+};
+use jni::JNIEnv;
+use log::{error, info};
+use num_traits::ToPrimitive;
+use uwb_uci_packets::{
+    GetCapsInfoRspPacket, Packet, SessionGetAppConfigRspPacket, SessionSetAppConfigRspPacket,
+    StatusCode, UciResponseChild, UciResponsePacket, UciVendor_9_ResponseChild,
+    UciVendor_A_ResponseChild, UciVendor_B_ResponseChild, UciVendor_E_ResponseChild,
+    UciVendor_F_ResponseChild,
+};
+use uwb_uci_rust::error::UwbErr;
+use uwb_uci_rust::event_manager::EventManagerImpl as EventManager;
+use uwb_uci_rust::uci::{uci_hrcv::UciResponse, Dispatcher, DispatcherImpl, JNICommand};
+
+trait Context<'a> {
+    fn convert_byte_array(&self, array: jbyteArray) -> Result<Vec<u8>, jni::errors::Error>;
+    fn get_array_length(&self, array: jarray) -> Result<jsize, jni::errors::Error>;
+    fn get_short_array_region(
+        &self,
+        array: jshortArray,
+        start: jsize,
+        buf: &mut [jshort],
+    ) -> Result<(), jni::errors::Error>;
+    fn get_int_array_region(
+        &self,
+        array: jintArray,
+        start: jsize,
+        buf: &mut [jint],
+    ) -> Result<(), jni::errors::Error>;
+    fn get_dispatcher(&self) -> Result<&'a mut dyn Dispatcher, UwbErr>;
+}
+
+struct JniContext<'a> {
+    env: JNIEnv<'a>,
+    obj: JObject<'a>,
+}
+
+impl<'a> JniContext<'a> {
+    fn new(env: JNIEnv<'a>, obj: JObject<'a>) -> Self {
+        Self { env, obj }
+    }
+}
+
+impl<'a> Context<'a> for JniContext<'a> {
+    fn convert_byte_array(&self, array: jbyteArray) -> Result<Vec<u8>, jni::errors::Error> {
+        self.env.convert_byte_array(array)
+    }
+    fn get_array_length(&self, array: jarray) -> Result<jsize, jni::errors::Error> {
+        self.env.get_array_length(array)
+    }
+    fn get_short_array_region(
+        &self,
+        array: jshortArray,
+        start: jsize,
+        buf: &mut [jshort],
+    ) -> Result<(), jni::errors::Error> {
+        self.env.get_short_array_region(array, start, buf)
+    }
+    fn get_int_array_region(
+        &self,
+        array: jintArray,
+        start: jsize,
+        buf: &mut [jint],
+    ) -> Result<(), jni::errors::Error> {
+        self.env.get_int_array_region(array, start, buf)
+    }
+    fn get_dispatcher(&self) -> Result<&'a mut dyn Dispatcher, UwbErr> {
+        let dispatcher_ptr_value = self.env.get_field(self.obj, "mDispatcherPointer", "J")?;
+        let dispatcher_ptr = dispatcher_ptr_value.j()?;
+        if dispatcher_ptr == 0i64 {
+            error!("The dispatcher is not initialized.");
+            return Err(UwbErr::NoneDispatcher);
+        }
+        // Safety: dispatcher pointer must not be a null pointer and it must point to a valid dispatcher object.
+        // This can be ensured because the dispatcher is created in an earlier stage and
+        // won't be deleted before calling doDeinitialize.
+        unsafe { Ok(&mut *(dispatcher_ptr as *mut DispatcherImpl)) }
+    }
+}
+
+/// Initialize UWB
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeInit(
+    _env: JNIEnv,
+    _obj: JObject,
+) -> jboolean {
+    logger::init(
+        logger::Config::default()
+            .with_tag_on_device("uwb")
+            .with_min_level(log::Level::Trace)
+            .with_filter("trace,jni=info"),
+    );
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeInit: enter");
+    true as jboolean
+}
+
+/// Get max session number
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeGetMaxSessionNumber(
+    _env: JNIEnv,
+    _obj: JObject,
+) -> jint {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeGetMaxSessionNumber: enter");
+    5
+}
+
+/// Turn on UWB. initialize the GKI module and HAL module for UWB device.
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeDoInitialize(
+    env: JNIEnv,
+    obj: JObject,
+) -> jboolean {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeDoInitialize: enter");
+    boolean_result_helper(do_initialize(&JniContext::new(env, obj)), "DoInitialize")
+}
+
+/// Turn off UWB. Deinitilize the GKI and HAL module, power of the UWB device.
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeDoDeinitialize(
+    env: JNIEnv,
+    obj: JObject,
+) -> jboolean {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeDoDeinitialize: enter");
+    boolean_result_helper(do_deinitialize(&JniContext::new(env, obj)), "DoDeinitialize")
+}
+
+/// get nanos
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeGetTimestampResolutionNanos(
+    _env: JNIEnv,
+    _obj: JObject,
+) -> jlong {
+    info!(
+        "Java_com_android_server_uwb_jni_NativeUwbManager_nativeGetTimestampResolutionNanos: enter"
+    );
+    0
+}
+
+/// reset the device
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeDeviceReset(
+    env: JNIEnv,
+    obj: JObject,
+    reset_config: jbyte,
+) -> jbyte {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeDeviceReset: enter");
+    byte_result_helper(reset_device(&JniContext::new(env, obj), reset_config as u8), "ResetDevice")
+}
+
+/// init the session
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeSessionInit(
+    env: JNIEnv,
+    obj: JObject,
+    session_id: jint,
+    session_type: jbyte,
+) -> jbyte {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeSessionInit: enter");
+    byte_result_helper(
+        session_init(&JniContext::new(env, obj), session_id as u32, session_type as u8),
+        "SessionInit",
+    )
+}
+
+/// deinit the session
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeSessionDeInit(
+    env: JNIEnv,
+    obj: JObject,
+    session_id: jint,
+) -> jbyte {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeSessionDeInit: enter");
+    byte_result_helper(
+        session_deinit(&JniContext::new(env, obj), session_id as u32),
+        "SessionDeInit",
+    )
+}
+
+/// get session count
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeGetSessionCount(
+    env: JNIEnv,
+    obj: JObject,
+) -> jbyte {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeGetSessionCount: enter");
+    match get_session_count(&JniContext::new(env, obj)) {
+        Ok(count) => count,
+        Err(e) => {
+            error!("GetSessionCount failed with {:?}", e);
+            -1
+        }
+    }
+}
+
+///  start the ranging
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeRangingStart(
+    env: JNIEnv,
+    obj: JObject,
+    session_id: jint,
+) -> jbyte {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeRangingStart: enter");
+    byte_result_helper(ranging_start(&JniContext::new(env, obj), session_id as u32), "RangingStart")
+}
+
+/// stop the ranging
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeRangingStop(
+    env: JNIEnv,
+    obj: JObject,
+    session_id: jint,
+) -> jbyte {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeRangingStop: enter");
+    byte_result_helper(ranging_stop(&JniContext::new(env, obj), session_id as u32), "RangingStop")
+}
+
+/// get the session state
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeGetSessionState(
+    env: JNIEnv,
+    obj: JObject,
+    session_id: jint,
+) -> jbyte {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeGetSessionState: enter");
+    match get_session_state(&JniContext::new(env, obj), session_id as u32) {
+        Ok(state) => state,
+        Err(e) => {
+            error!("GetSessionState failed with {:?}", e);
+            -1
+        }
+    }
+}
+
+/// set app configurations
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeSetAppConfigurations(
+    env: JNIEnv,
+    obj: JObject,
+    session_id: jint,
+    no_of_params: jint,
+    app_config_param_len: jint,
+    app_config_params: jbyteArray,
+) -> jbyteArray {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeSetAppConfigurations: enter");
+    match set_app_configurations(
+        &JniContext::new(env, obj),
+        session_id as u32,
+        no_of_params as u32,
+        app_config_param_len as u32,
+        app_config_params,
+    ) {
+        Ok(data) => {
+            let uwb_config_status_class =
+                env.find_class("com/android/server/uwb/data/UwbConfigStatusData").unwrap();
+            let mut buf: Vec<u8> = Vec::new();
+            for iter in data.get_cfg_status() {
+                buf.push(iter.cfg_id as u8);
+                buf.push(iter.status as u8);
+            }
+            let cfg_jbytearray = env.byte_array_from_slice(&buf).unwrap();
+            let uwb_config_status_object = env.new_object(
+                uwb_config_status_class,
+                "(II[B)V",
+                &[
+                    JValue::Int(data.get_status().to_i32().unwrap()),
+                    JValue::Int(data.get_cfg_status().len().to_i32().unwrap()),
+                    JValue::Object(JObject::from(cfg_jbytearray)),
+                ],
+            );
+            *uwb_config_status_object.unwrap()
+        }
+        Err(e) => {
+            error!("SetAppConfig failed with: {:?}", e);
+            *JObject::null()
+        }
+    }
+}
+
+/// get app configurations
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeGetAppConfigurations(
+    env: JNIEnv,
+    obj: JObject,
+    session_id: jint,
+    no_of_params: jint,
+    app_config_param_len: jint,
+    app_config_params: jbyteArray,
+) -> jbyteArray {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeGetAppConfigurations: enter");
+    match get_app_configurations(
+        &JniContext::new(env, obj),
+        session_id as u32,
+        no_of_params as u32,
+        app_config_param_len as u32,
+        app_config_params,
+    ) {
+        Ok(data) => {
+            let uwb_tlv_info_class =
+                env.find_class("com/android/server/uwb/data/UwbTlvData").unwrap();
+            let mut buf: Vec<u8> = Vec::new();
+            for tlv in data.get_tlvs() {
+                buf.push(tlv.cfg_id as u8);
+                buf.push(tlv.v.len() as u8);
+                buf.extend(&tlv.v);
+            }
+            let tlv_jbytearray = env.byte_array_from_slice(&buf).unwrap();
+            let uwb_tlv_info_object = env.new_object(
+                uwb_tlv_info_class,
+                "(II[B)V",
+                &[
+                    JValue::Int(data.get_status().to_i32().unwrap()),
+                    JValue::Int(data.get_tlvs().len().to_i32().unwrap()),
+                    JValue::Object(JObject::from(tlv_jbytearray)),
+                ],
+            );
+            *uwb_tlv_info_object.unwrap()
+        }
+        Err(e) => {
+            error!("GetAppConfig failed with: {:?}", e);
+            *JObject::null()
+        }
+    }
+}
+
+/// get capability info
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeGetCapsInfo(
+    env: JNIEnv,
+    obj: JObject,
+) -> jbyteArray {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeGetCapsInfo: enter");
+    match get_caps_info(&JniContext::new(env, obj)) {
+        Ok(data) => {
+            let uwb_tlv_info_class =
+                env.find_class("com/android/server/uwb/data/UwbTlvData").unwrap();
+            let mut buf: Vec<u8> = Vec::new();
+            for tlv in data.get_tlvs() {
+                buf.push(tlv.t as u8);
+                buf.push(tlv.v.len() as u8);
+                buf.extend(&tlv.v);
+            }
+            let tlv_jbytearray = env.byte_array_from_slice(&buf).unwrap();
+            let uwb_tlv_info_object = env.new_object(
+                uwb_tlv_info_class,
+                "(II[B)V",
+                &[
+                    JValue::Int(data.get_status().to_i32().unwrap()),
+                    JValue::Int(data.get_tlvs().len().to_i32().unwrap()),
+                    JValue::Object(JObject::from(tlv_jbytearray)),
+                ],
+            );
+            *uwb_tlv_info_object.unwrap()
+        }
+        Err(e) => {
+            error!("GetCapsInfo failed with: {:?}", e);
+            *JObject::null()
+        }
+    }
+}
+
+/// update multicast list
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeControllerMulticastListUpdate(
+    env: JNIEnv,
+    obj: JObject,
+    session_id: jint,
+    action: jbyte,
+    no_of_controlee: jbyte,
+    addresses: jshortArray,
+    sub_session_ids: jintArray,
+) -> jbyte {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeControllerMulticastListUpdate: enter");
+    byte_result_helper(
+        multicast_list_update(
+            &JniContext::new(env, obj),
+            session_id as u32,
+            action as u8,
+            no_of_controlee as u8,
+            addresses,
+            sub_session_ids,
+        ),
+        "ControllerMulticastListUpdate",
+    )
+}
+
+/// set country code
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeSetCountryCode(
+    env: JNIEnv,
+    obj: JObject,
+    country_code: jbyteArray,
+) -> jbyte {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeSetCountryCode: enter");
+    byte_result_helper(set_country_code(&JniContext::new(env, obj), country_code), "SetCountryCode")
+}
+
+/// set country code
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeSendRawVendorCmd(
+    env: JNIEnv,
+    obj: JObject,
+    gid: jint,
+    oid: jint,
+    payload: jbyteArray,
+) -> jobject {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeRawVendor: enter");
+    let uwb_vendor_uci_response_class =
+        env.find_class("com/android/server/uwb/data/UwbVendorUciResponse").unwrap();
+    match send_raw_vendor_cmd(
+        &JniContext::new(env, obj),
+        gid.try_into().expect("invalid gid"),
+        oid.try_into().expect("invalid oid"),
+        payload,
+    ) {
+        Ok((gid, oid, payload)) => *env
+            .new_object(
+                uwb_vendor_uci_response_class,
+                "(BII[B)V",
+                &[
+                    JValue::Byte(StatusCode::UciStatusOk.to_i8().unwrap()),
+                    JValue::Int(gid.to_i32().unwrap()),
+                    JValue::Int(oid.to_i32().unwrap()),
+                    JValue::Object(JObject::from(
+                        env.byte_array_from_slice(payload.as_ref()).unwrap(),
+                    )),
+                ],
+            )
+            .unwrap(),
+        Err(e) => {
+            error!("send raw uci cmd failed with: {:?}", e);
+            *env.new_object(
+                uwb_vendor_uci_response_class,
+                "(BII[B)V",
+                &[
+                    JValue::Byte(StatusCode::UciStatusFailed.to_i8().unwrap()),
+                    JValue::Int(-1),
+                    JValue::Int(-1),
+                    JValue::Object(JObject::null()),
+                ],
+            )
+            .unwrap()
+        }
+    }
+}
+
+/// retrieve the UWB power stats
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeGetPowerStats(
+    env: JNIEnv,
+    obj: JObject,
+) -> jobject {
+    info!("Java_com_android_server_uwb_jni_NativeUwbManager_nativeGetPowerStats: enter");
+    let uwb_power_stats_class =
+        env.find_class("com/android/server/uwb/info/UwbPowerStats").unwrap();
+    match get_power_stats(&JniContext::new(env, obj)) {
+        Ok(para) => {
+            let power_stats = env.new_object(uwb_power_stats_class, "(IIII)V", &para).unwrap();
+            *power_stats
+        }
+        Err(e) => {
+            error!("Get power stats failed with: {:?}", e);
+            *JObject::null()
+        }
+    }
+}
+
+fn boolean_result_helper(result: Result<(), UwbErr>, function_name: &str) -> jboolean {
+    match result {
+        Ok(()) => true as jboolean,
+        Err(err) => {
+            error!("{} failed with: {:?}", function_name, err);
+            false as jboolean
+        }
+    }
+}
+
+fn byte_result_helper(result: Result<(), UwbErr>, function_name: &str) -> jbyte {
+    match result {
+        Ok(()) => StatusCode::UciStatusOk.to_i8().unwrap(),
+        Err(err) => {
+            error!("{} failed with: {:?}", function_name, err);
+            match err {
+                UwbErr::StatusCode(status_code) => status_code
+                    .to_i8()
+                    .unwrap_or_else(|| StatusCode::UciStatusFailed.to_i8().unwrap()),
+                _ => StatusCode::UciStatusFailed.to_i8().unwrap(),
+            }
+        }
+    }
+}
+
+fn do_initialize<'a, T: Context<'a>>(context: &T) -> Result<(), UwbErr> {
+    let dispatcher = context.get_dispatcher()?;
+    dispatcher.send_jni_command(JNICommand::Enable)?;
+    match uwa_get_device_info(dispatcher) {
+        Ok(res) => {
+            if let UciResponse::GetDeviceInfoRsp(device_info) = res {
+                dispatcher.set_device_info(Some(device_info));
+            }
+        }
+        Err(e) => {
+            error!("GetDeviceInfo failed with: {:?}", e);
+            return Err(UwbErr::failed());
+        }
+    }
+    Ok(())
+}
+
+fn do_deinitialize<'a, T: Context<'a>>(context: &T) -> Result<(), UwbErr> {
+    let dispatcher = context.get_dispatcher()?;
+    dispatcher.send_jni_command(JNICommand::Disable(true))?;
+    dispatcher.wait_for_exit()?;
+    Ok(())
+}
+
+// unused, but leaving this behind if we want to use it later.
+#[allow(dead_code)]
+fn get_specification_info<'a, T: Context<'a>>(context: &T) -> Result<[JValue<'a>; 16], UwbErr> {
+    let dispatcher = context.get_dispatcher()?;
+    match dispatcher.get_device_info() {
+        Some(data) => {
+            Ok([
+                JValue::Int((data.get_uci_version() & 0xFF).into()),
+                JValue::Int(((data.get_uci_version() >> 8) & 0xF).into()),
+                JValue::Int(((data.get_uci_version() >> 12) & 0xF).into()),
+                JValue::Int((data.get_mac_version() & 0xFF).into()),
+                JValue::Int(((data.get_mac_version() >> 8) & 0xF).into()),
+                JValue::Int(((data.get_mac_version() >> 12) & 0xF).into()),
+                JValue::Int((data.get_phy_version() & 0xFF).into()),
+                JValue::Int(((data.get_phy_version() >> 8) & 0xF).into()),
+                JValue::Int(((data.get_phy_version() >> 12) & 0xF).into()),
+                JValue::Int((data.get_uci_test_version() & 0xFF).into()),
+                JValue::Int(((data.get_uci_test_version() >> 8) & 0xF).into()),
+                JValue::Int(((data.get_uci_test_version() >> 12) & 0xF).into()),
+                JValue::Int(1), // fira_major_version
+                JValue::Int(0), // fira_minor_version
+                JValue::Int(1), // ccc_major_version
+                JValue::Int(0), // ccc_minor_version
+            ])
+        }
+        None => {
+            error!("Fail to get specification info.");
+            Err(UwbErr::failed())
+        }
+    }
+}
+
+fn session_init<'a, T: Context<'a>>(
+    context: &T,
+    session_id: u32,
+    session_type: u8,
+) -> Result<(), UwbErr> {
+    let dispatcher = context.get_dispatcher()?;
+    let res = match dispatcher
+        .block_on_jni_command(JNICommand::UciSessionInit(session_id, session_type))?
+    {
+        UciResponse::SessionInitRsp(data) => data,
+        _ => return Err(UwbErr::failed()),
+    };
+    status_code_to_res(res.get_status())
+}
+
+fn session_deinit<'a, T: Context<'a>>(context: &T, session_id: u32) -> Result<(), UwbErr> {
+    let dispatcher = context.get_dispatcher()?;
+    let res = match dispatcher.block_on_jni_command(JNICommand::UciSessionDeinit(session_id))? {
+        UciResponse::SessionDeinitRsp(data) => data,
+        _ => return Err(UwbErr::failed()),
+    };
+    status_code_to_res(res.get_status())
+}
+
+fn get_session_count<'a, T: Context<'a>>(context: &T) -> Result<jbyte, UwbErr> {
+    let dispatcher = context.get_dispatcher()?;
+    match dispatcher.block_on_jni_command(JNICommand::UciSessionGetCount)? {
+        UciResponse::SessionGetCountRsp(rsp) => match status_code_to_res(rsp.get_status()) {
+            Ok(()) => Ok(rsp.get_session_count() as jbyte),
+            Err(err) => Err(err),
+        },
+        _ => Err(UwbErr::failed()),
+    }
+}
+
+fn ranging_start<'a, T: Context<'a>>(context: &T, session_id: u32) -> Result<(), UwbErr> {
+    let dispatcher = context.get_dispatcher()?;
+    let res = match dispatcher.block_on_jni_command(JNICommand::UciStartRange(session_id))? {
+        UciResponse::RangeStartRsp(data) => data,
+        _ => return Err(UwbErr::failed()),
+    };
+    status_code_to_res(res.get_status())
+}
+
+fn ranging_stop<'a, T: Context<'a>>(context: &T, session_id: u32) -> Result<(), UwbErr> {
+    let dispatcher = context.get_dispatcher()?;
+    let res = match dispatcher.block_on_jni_command(JNICommand::UciStopRange(session_id))? {
+        UciResponse::RangeStopRsp(data) => data,
+        _ => return Err(UwbErr::failed()),
+    };
+    status_code_to_res(res.get_status())
+}
+
+fn get_session_state<'a, T: Context<'a>>(context: &T, session_id: u32) -> Result<jbyte, UwbErr> {
+    let dispatcher = context.get_dispatcher()?;
+    match dispatcher.block_on_jni_command(JNICommand::UciGetSessionState(session_id))? {
+        UciResponse::SessionGetStateRsp(data) => Ok(data.get_session_state() as jbyte),
+        _ => Err(UwbErr::failed()),
+    }
+}
+
+fn set_app_configurations<'a, T: Context<'a>>(
+    context: &T,
+    session_id: u32,
+    no_of_params: u32,
+    app_config_param_len: u32,
+    app_config_params: jintArray,
+) -> Result<SessionSetAppConfigRspPacket, UwbErr> {
+    let app_configs = context.convert_byte_array(app_config_params)?;
+    let dispatcher = context.get_dispatcher()?;
+    match dispatcher.block_on_jni_command(JNICommand::UciSetAppConfig {
+        session_id,
+        no_of_params,
+        app_config_param_len,
+        app_configs,
+    })? {
+        UciResponse::SessionSetAppConfigRsp(data) => Ok(data),
+        _ => Err(UwbErr::failed()),
+    }
+}
+
+fn get_app_configurations<'a, T: Context<'a>>(
+    context: &T,
+    session_id: u32,
+    no_of_params: u32,
+    app_config_param_len: u32,
+    app_config_params: jintArray,
+) -> Result<SessionGetAppConfigRspPacket, UwbErr> {
+    let app_configs = context.convert_byte_array(app_config_params)?;
+    let dispatcher = context.get_dispatcher()?;
+    match dispatcher.block_on_jni_command(JNICommand::UciGetAppConfig {
+        session_id,
+        no_of_params,
+        app_config_param_len,
+        app_configs,
+    })? {
+        UciResponse::SessionGetAppConfigRsp(data) => Ok(data),
+        _ => Err(UwbErr::failed()),
+    }
+}
+
+fn get_caps_info<'a, T: Context<'a>>(context: &T) -> Result<GetCapsInfoRspPacket, UwbErr> {
+    let dispatcher = context.get_dispatcher()?;
+    match dispatcher.block_on_jni_command(JNICommand::UciGetCapsInfo)? {
+        UciResponse::GetCapsInfoRsp(data) => Ok(data),
+        _ => Err(UwbErr::failed()),
+    }
+}
+
+fn multicast_list_update<'a, T: Context<'a>>(
+    context: &T,
+    session_id: u32,
+    action: u8,
+    no_of_controlee: u8,
+    addresses: jshortArray,
+    sub_session_ids: jintArray,
+) -> Result<(), UwbErr> {
+    let mut address_list = vec![0i16; context.get_array_length(addresses)?.try_into().unwrap()];
+    context.get_short_array_region(addresses, 0, &mut address_list)?;
+    let mut sub_session_id_list =
+        vec![0i32; context.get_array_length(sub_session_ids)?.try_into().unwrap()];
+    context.get_int_array_region(sub_session_ids, 0, &mut sub_session_id_list)?;
+    let dispatcher = context.get_dispatcher()?;
+    let res = match dispatcher.block_on_jni_command(JNICommand::UciSessionUpdateMulticastList {
+        session_id,
+        action,
+        no_of_controlee,
+        address_list: address_list.to_vec(),
+        sub_session_id_list: sub_session_id_list.to_vec(),
+    })? {
+        UciResponse::SessionUpdateControllerMulticastListRsp(data) => data,
+        _ => return Err(UwbErr::failed()),
+    };
+    status_code_to_res(res.get_status())
+}
+
+fn set_country_code<'a, T: Context<'a>>(
+    context: &T,
+    country_code: jbyteArray,
+) -> Result<(), UwbErr> {
+    let code = context.convert_byte_array(country_code)?;
+    if code.len() != 2 {
+        return Err(UwbErr::failed());
+    }
+    let dispatcher = context.get_dispatcher()?;
+    let res = match dispatcher.block_on_jni_command(JNICommand::UciSetCountryCode { code })? {
+        UciResponse::AndroidSetCountryCodeRsp(data) => data,
+        _ => return Err(UwbErr::failed()),
+    };
+    status_code_to_res(res.get_status())
+}
+
+fn get_vendor_uci_payload(data: UciResponsePacket) -> Result<Vec<u8>, UwbErr> {
+    match data.specialize() {
+        UciResponseChild::UciVendor_9_Response(evt) => match evt.specialize() {
+            UciVendor_9_ResponseChild::Payload(payload) => Ok(payload.to_vec()),
+            UciVendor_9_ResponseChild::None => Ok(Vec::new()),
+        },
+        UciResponseChild::UciVendor_A_Response(evt) => match evt.specialize() {
+            UciVendor_A_ResponseChild::Payload(payload) => Ok(payload.to_vec()),
+            UciVendor_A_ResponseChild::None => Ok(Vec::new()),
+        },
+        UciResponseChild::UciVendor_B_Response(evt) => match evt.specialize() {
+            UciVendor_B_ResponseChild::Payload(payload) => Ok(payload.to_vec()),
+            UciVendor_B_ResponseChild::None => Ok(Vec::new()),
+        },
+        UciResponseChild::UciVendor_E_Response(evt) => match evt.specialize() {
+            UciVendor_E_ResponseChild::Payload(payload) => Ok(payload.to_vec()),
+            UciVendor_E_ResponseChild::None => Ok(Vec::new()),
+        },
+        UciResponseChild::UciVendor_F_Response(evt) => match evt.specialize() {
+            UciVendor_F_ResponseChild::Payload(payload) => Ok(payload.to_vec()),
+            UciVendor_F_ResponseChild::None => Ok(Vec::new()),
+        },
+        _ => {
+            error!("Invalid vendor response with gid {:?}", data.get_group_id());
+            Err(UwbErr::Specialize(data.to_vec()))
+        }
+    }
+}
+
+fn send_raw_vendor_cmd<'a, T: Context<'a>>(
+    context: &T,
+    gid: u32,
+    oid: u32,
+    payload: jbyteArray,
+) -> Result<(i32, i32, Vec<u8>), UwbErr> {
+    let payload = context.convert_byte_array(payload)?;
+    let dispatcher = context.get_dispatcher()?;
+    match dispatcher.block_on_jni_command(JNICommand::UciRawVendorCmd { gid, oid, payload })? {
+        UciResponse::RawVendorRsp(response) => Ok((
+            response.get_group_id().to_i32().unwrap(),
+            response.get_opcode().to_i32().unwrap(),
+            get_vendor_uci_payload(response)?,
+        )),
+        _ => Err(UwbErr::failed()),
+    }
+}
+
+fn status_code_to_res(status_code: StatusCode) -> Result<(), UwbErr> {
+    match status_code {
+        StatusCode::UciStatusOk => Ok(()),
+        _ => Err(UwbErr::StatusCode(status_code)),
+    }
+}
+
+/// create a dispatcher instance
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeDispatcherNew(
+    env: JNIEnv,
+    obj: JObject,
+) -> jlong {
+    let eventmanager = match EventManager::new(env, obj) {
+        Ok(evtmgr) => evtmgr,
+        Err(err) => {
+            error!("Fail to create event manager{:?}", err);
+            return *JObject::null() as jlong;
+        }
+    };
+    match DispatcherImpl::new(eventmanager) {
+        Ok(dispatcher) => Box::into_raw(Box::new(dispatcher)) as jlong,
+        Err(err) => {
+            error!("Fail to create dispatcher {:?}", err);
+            *JObject::null() as jlong
+        }
+    }
+}
+
+/// destroy the dispatcher instance
+#[no_mangle]
+pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeDispatcherDestroy(
+    env: JNIEnv,
+    obj: JObject,
+) {
+    let dispatcher_ptr_value = match env.get_field(obj, "mDispatcherPointer", "J") {
+        Ok(value) => value,
+        Err(err) => {
+            error!("Failed to get the pointer with: {:?}", err);
+            return;
+        }
+    };
+    let dispatcher_ptr = match dispatcher_ptr_value.j() {
+        Ok(value) => value,
+        Err(err) => {
+            error!("Failed to get the pointer with: {:?}", err);
+            return;
+        }
+    };
+    // Safety: dispatcher pointer must not be a null pointer and must point to a valid dispatcher object.
+    // This can be ensured because the dispatcher is created in an earlier stage and
+    // won't be deleted before calling this destroy function.
+    // This function will early return if the instance is already destroyed.
+    let _boxed_dispatcher = unsafe { Box::from_raw(dispatcher_ptr as *mut DispatcherImpl) };
+    info!("The dispatcher successfully destroyed.");
+}
+
+fn get_power_stats<'a, T: Context<'a>>(context: &T) -> Result<[JValue<'a>; 4], UwbErr> {
+    let dispatcher = context.get_dispatcher()?;
+    match dispatcher.block_on_jni_command(JNICommand::UciGetPowerStats)? {
+        UciResponse::AndroidGetPowerStatsRsp(data) => Ok([
+            JValue::Int(data.get_stats().idle_time_ms as i32),
+            JValue::Int(data.get_stats().tx_time_ms as i32),
+            JValue::Int(data.get_stats().rx_time_ms as i32),
+            JValue::Int(data.get_stats().total_wake_count as i32),
+        ]),
+        _ => Err(UwbErr::failed()),
+    }
+}
+
+fn uwa_get_device_info(dispatcher: &dyn Dispatcher) -> Result<UciResponse, UwbErr> {
+    let res = dispatcher.block_on_jni_command(JNICommand::UciGetDeviceInfo)?;
+    Ok(res)
+}
+
+fn reset_device<'a, T: Context<'a>>(context: &T, reset_config: u8) -> Result<(), UwbErr> {
+    let dispatcher = context.get_dispatcher()?;
+    let res = match dispatcher.block_on_jni_command(JNICommand::UciDeviceReset { reset_config })? {
+        UciResponse::DeviceResetRsp(data) => data,
+        _ => return Err(UwbErr::failed()),
+    };
+    status_code_to_res(res.get_status())
+}
+
+#[cfg(test)]
+mod mock_context;
+#[cfg(test)]
+mod mock_dispatcher;
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use crate::mock_context::MockContext;
+    use crate::mock_dispatcher::MockDispatcher;
+
+    #[test]
+    fn test_boolean_result_helper() {
+        assert_eq!(true as jboolean, boolean_result_helper(Ok(()), "Foo"));
+        assert_eq!(false as jboolean, boolean_result_helper(Err(UwbErr::Undefined), "Foo"));
+    }
+
+    #[test]
+    fn test_byte_result_helper() {
+        assert_eq!(StatusCode::UciStatusOk.to_i8().unwrap(), byte_result_helper(Ok(()), "Foo"));
+        assert_eq!(
+            StatusCode::UciStatusFailed.to_i8().unwrap(),
+            byte_result_helper(Err(UwbErr::Undefined), "Foo")
+        );
+        assert_eq!(
+            StatusCode::UciStatusRejected.to_i8().unwrap(),
+            byte_result_helper(Err(UwbErr::StatusCode(StatusCode::UciStatusRejected)), "Foo")
+        );
+    }
+
+    #[test]
+    fn test_do_initialize() {
+        let packet = uwb_uci_packets::GetDeviceInfoRspBuilder {
+            status: StatusCode::UciStatusOk,
+            uci_version: 0,
+            mac_version: 0,
+            phy_version: 0,
+            uci_test_version: 0,
+            vendor_spec_info: vec![],
+        }
+        .build();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_send_jni_command(JNICommand::Enable, Ok(()));
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciGetDeviceInfo,
+            Ok(UciResponse::GetDeviceInfoRsp(packet.clone())),
+        );
+        let mut context = MockContext::new(dispatcher);
+
+        let result = do_initialize(&context);
+        let device_info = context.get_mock_dispatcher().get_device_info().clone();
+        assert!(result.is_ok());
+        assert_eq!(device_info.unwrap().to_vec(), packet.to_vec());
+    }
+
+    #[test]
+    fn test_do_deinitialize() {
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_send_jni_command(JNICommand::Disable(true), Ok(()));
+        dispatcher.expect_wait_for_exit(Ok(()));
+        let context = MockContext::new(dispatcher);
+
+        let result = do_deinitialize(&context);
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_get_specification_info() {
+        let packet = uwb_uci_packets::GetDeviceInfoRspBuilder {
+            status: StatusCode::UciStatusOk,
+            uci_version: 0x1234,
+            mac_version: 0x5678,
+            phy_version: 0x9ABC,
+            uci_test_version: 0x1357,
+            vendor_spec_info: vec![],
+        }
+        .build();
+        let expected_array = [
+            0x34, 0x2, 0x1, // uci_version
+            0x78, 0x6, 0x5, // mac_version.
+            0xBC, 0xA, 0x9, // phy_version.
+            0x57, 0x3, 0x1, // uci_test_version.
+            1,   // fira_major_version
+            0,   // fira_minor_version
+            1,   // ccc_major_version
+            0,   // ccc_minor_version
+        ];
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.set_device_info(Some(packet));
+        let context = MockContext::new(dispatcher);
+
+        let results = get_specification_info(&context).unwrap();
+        for (idx, result) in results.iter().enumerate() {
+            assert_eq!(TryInto::<jint>::try_into(*result).unwrap(), expected_array[idx]);
+        }
+    }
+
+    #[test]
+    fn test_session_init() {
+        let session_id = 1234;
+        let session_type = 5;
+        let packet =
+            uwb_uci_packets::SessionInitRspBuilder { status: StatusCode::UciStatusOk }.build();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciSessionInit(session_id, session_type),
+            Ok(UciResponse::SessionInitRsp(packet)),
+        );
+        let context = MockContext::new(dispatcher);
+
+        let result = session_init(&context, session_id, session_type);
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_session_deinit() {
+        let session_id = 1234;
+        let packet =
+            uwb_uci_packets::SessionDeinitRspBuilder { status: StatusCode::UciStatusOk }.build();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciSessionDeinit(session_id),
+            Ok(UciResponse::SessionDeinitRsp(packet)),
+        );
+        let context = MockContext::new(dispatcher);
+
+        let result = session_deinit(&context, session_id);
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_get_session_count() {
+        let session_count = 7;
+        let packet = uwb_uci_packets::SessionGetCountRspBuilder {
+            status: StatusCode::UciStatusOk,
+            session_count,
+        }
+        .build();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciSessionGetCount,
+            Ok(UciResponse::SessionGetCountRsp(packet)),
+        );
+        let context = MockContext::new(dispatcher);
+
+        let result = get_session_count(&context).unwrap();
+        assert_eq!(result, session_count as jbyte);
+    }
+
+    #[test]
+    fn test_ranging_start() {
+        let session_id = 1234;
+        let packet =
+            uwb_uci_packets::RangeStartRspBuilder { status: StatusCode::UciStatusOk }.build();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciStartRange(session_id),
+            Ok(UciResponse::RangeStartRsp(packet)),
+        );
+        let context = MockContext::new(dispatcher);
+
+        let result = ranging_start(&context, session_id);
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_ranging_stop() {
+        let session_id = 1234;
+        let packet =
+            uwb_uci_packets::RangeStopRspBuilder { status: StatusCode::UciStatusOk }.build();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciStopRange(session_id),
+            Ok(UciResponse::RangeStopRsp(packet)),
+        );
+        let context = MockContext::new(dispatcher);
+
+        let result = ranging_stop(&context, session_id);
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_get_session_state() {
+        let session_id = 1234;
+        let session_state = uwb_uci_packets::SessionState::SessionStateActive;
+        let packet = uwb_uci_packets::SessionGetStateRspBuilder {
+            status: StatusCode::UciStatusOk,
+            session_state,
+        }
+        .build();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciGetSessionState(session_id),
+            Ok(UciResponse::SessionGetStateRsp(packet)),
+        );
+        let context = MockContext::new(dispatcher);
+
+        let result = get_session_state(&context, session_id).unwrap();
+        assert_eq!(result, session_state as jbyte);
+    }
+
+    #[test]
+    fn test_set_app_configurations() {
+        let session_id = 1234;
+        let no_of_params = 3;
+        let app_config_param_len = 5;
+        let app_configs = vec![1, 2, 3, 4, 5];
+        let fake_app_config_params = std::ptr::null_mut();
+        let packet = uwb_uci_packets::SessionSetAppConfigRspBuilder {
+            status: StatusCode::UciStatusOk,
+            cfg_status: vec![],
+        }
+        .build();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciSetAppConfig {
+                session_id,
+                no_of_params,
+                app_config_param_len,
+                app_configs: app_configs.clone(),
+            },
+            Ok(UciResponse::SessionSetAppConfigRsp(packet.clone())),
+        );
+        let mut context = MockContext::new(dispatcher);
+        context.expect_convert_byte_array(fake_app_config_params, Ok(app_configs));
+
+        let result = set_app_configurations(
+            &context,
+            session_id,
+            no_of_params,
+            app_config_param_len,
+            fake_app_config_params,
+        )
+        .unwrap();
+        assert_eq!(result.to_vec(), packet.to_vec());
+    }
+
+    #[test]
+    fn test_get_app_configurations() {
+        let session_id = 1234;
+        let no_of_params = 3;
+        let app_config_param_len = 5;
+        let app_configs = vec![1, 2, 3, 4, 5];
+        let fake_app_config_params = std::ptr::null_mut();
+        let packet = uwb_uci_packets::SessionGetAppConfigRspBuilder {
+            status: StatusCode::UciStatusOk,
+            tlvs: vec![],
+        }
+        .build();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciGetAppConfig {
+                session_id,
+                no_of_params,
+                app_config_param_len,
+                app_configs: app_configs.clone(),
+            },
+            Ok(UciResponse::SessionGetAppConfigRsp(packet.clone())),
+        );
+        let mut context = MockContext::new(dispatcher);
+        context.expect_convert_byte_array(fake_app_config_params, Ok(app_configs));
+
+        let result = get_app_configurations(
+            &context,
+            session_id,
+            no_of_params,
+            app_config_param_len,
+            fake_app_config_params,
+        )
+        .unwrap();
+        assert_eq!(result.to_vec(), packet.to_vec());
+    }
+
+    #[test]
+    fn test_get_caps_info() {
+        let packet = uwb_uci_packets::GetCapsInfoRspBuilder {
+            status: StatusCode::UciStatusOk,
+            tlvs: vec![],
+        }
+        .build();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciGetCapsInfo,
+            Ok(UciResponse::GetCapsInfoRsp(packet.clone())),
+        );
+        let context = MockContext::new(dispatcher);
+
+        let result = get_caps_info(&context).unwrap();
+        assert_eq!(result.to_vec(), packet.to_vec());
+    }
+
+    #[test]
+    fn test_multicast_list_update() {
+        let session_id = 1234;
+        let action = 3;
+        let no_of_controlee = 5;
+        let fake_addresses = std::ptr::null_mut();
+        let address_list = Box::new([1, 3, 5, 7, 9]);
+        let fake_sub_session_ids = std::ptr::null_mut();
+        let sub_session_id_list = Box::new([2, 4, 6, 8, 10]);
+        let packet = uwb_uci_packets::SessionUpdateControllerMulticastListRspBuilder {
+            status: StatusCode::UciStatusOk,
+        }
+        .build();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciSessionUpdateMulticastList {
+                session_id,
+                action,
+                no_of_controlee,
+                address_list: address_list.to_vec(),
+                sub_session_id_list: sub_session_id_list.to_vec(),
+            },
+            Ok(UciResponse::SessionUpdateControllerMulticastListRsp(packet)),
+        );
+        let mut context = MockContext::new(dispatcher);
+        context.expect_get_array_length(fake_addresses, Ok(address_list.len() as jsize));
+        context.expect_get_short_array_region(fake_addresses, 0, Ok(address_list));
+        context
+            .expect_get_array_length(fake_sub_session_ids, Ok(sub_session_id_list.len() as jsize));
+        context.expect_get_int_array_region(fake_sub_session_ids, 0, Ok(sub_session_id_list));
+
+        let result = multicast_list_update(
+            &context,
+            session_id,
+            action,
+            no_of_controlee,
+            fake_addresses,
+            fake_sub_session_ids,
+        );
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_set_country_code() {
+        let fake_country_code = std::ptr::null_mut();
+        let country_code = "US".as_bytes().to_vec();
+        let packet =
+            uwb_uci_packets::AndroidSetCountryCodeRspBuilder { status: StatusCode::UciStatusOk }
+                .build();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciSetCountryCode { code: country_code.clone() },
+            Ok(UciResponse::AndroidSetCountryCodeRsp(packet)),
+        );
+        let mut context = MockContext::new(dispatcher);
+        context.expect_convert_byte_array(fake_country_code, Ok(country_code));
+
+        let result = set_country_code(&context, fake_country_code);
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_send_raw_vendor_cmd() {
+        let gid = 2;
+        let oid = 4;
+        let opcode = 6;
+        let fake_payload = std::ptr::null_mut();
+        let payload = vec![1, 2, 4, 8];
+        let response = vec![3, 6, 9];
+        let packet = uwb_uci_packets::UciVendor_9_ResponseBuilder {
+            opcode,
+            payload: Some(response.clone().into()),
+        }
+        .build()
+        .into();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciRawVendorCmd { gid, oid, payload: payload.clone() },
+            Ok(UciResponse::RawVendorRsp(packet)),
+        );
+        let mut context = MockContext::new(dispatcher);
+        context.expect_convert_byte_array(fake_payload, Ok(payload));
+
+        let result = send_raw_vendor_cmd(&context, gid, oid, fake_payload).unwrap();
+        assert_eq!(result.0, uwb_uci_packets::GroupId::VendorReserved9 as i32);
+        assert_eq!(result.1, opcode as i32);
+        assert_eq!(result.2, response);
+    }
+
+    #[test]
+    fn test_get_power_stats() {
+        let idle_time_ms = 5;
+        let tx_time_ms = 4;
+        let rx_time_ms = 3;
+        let total_wake_count = 2;
+        let packet = uwb_uci_packets::AndroidGetPowerStatsRspBuilder {
+            stats: uwb_uci_packets::PowerStats {
+                status: StatusCode::UciStatusOk,
+                idle_time_ms,
+                tx_time_ms,
+                rx_time_ms,
+                total_wake_count,
+            },
+        }
+        .build();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciGetPowerStats,
+            Ok(UciResponse::AndroidGetPowerStatsRsp(packet)),
+        );
+        let context = MockContext::new(dispatcher);
+
+        let result = get_power_stats(&context).unwrap();
+        assert_eq!(TryInto::<jint>::try_into(result[0]).unwrap(), idle_time_ms as jint);
+        assert_eq!(TryInto::<jint>::try_into(result[1]).unwrap(), tx_time_ms as jint);
+        assert_eq!(TryInto::<jint>::try_into(result[2]).unwrap(), rx_time_ms as jint);
+        assert_eq!(TryInto::<jint>::try_into(result[3]).unwrap(), total_wake_count as jint);
+    }
+
+    #[test]
+    fn test_reset_device() {
+        let reset_config = uwb_uci_packets::ResetConfig::UwbsReset as u8;
+        let packet =
+            uwb_uci_packets::DeviceResetRspBuilder { status: StatusCode::UciStatusOk }.build();
+
+        let mut dispatcher = MockDispatcher::new();
+        dispatcher.expect_block_on_jni_command(
+            JNICommand::UciDeviceReset { reset_config },
+            Ok(UciResponse::DeviceResetRsp(packet)),
+        );
+        let context = MockContext::new(dispatcher);
+
+        let result = reset_device(&context, reset_config);
+        assert!(result.is_ok());
+    }
+}
diff --git a/service/uci/jni/rust/mock_context.rs b/service/uci/jni/rust/mock_context.rs
new file mode 100644
index 0000000..60fb9b1
--- /dev/null
+++ b/service/uci/jni/rust/mock_context.rs
@@ -0,0 +1,187 @@
+use std::cell::{Cell, RefCell};
+use std::collections::VecDeque;
+
+use jni::sys::{jarray, jbyteArray, jint, jintArray, jshort, jshortArray, jsize};
+use uwb_uci_rust::error::UwbErr;
+use uwb_uci_rust::uci::Dispatcher;
+
+use crate::mock_dispatcher::MockDispatcher;
+use crate::Context;
+
+#[cfg(test)]
+pub struct MockContext {
+    dispatcher: Cell<MockDispatcher>,
+    expected_calls: RefCell<VecDeque<ExpectedCall>>,
+}
+
+#[cfg(test)]
+impl MockContext {
+    pub fn new(dispatcher: MockDispatcher) -> Self {
+        Self { dispatcher: Cell::new(dispatcher), expected_calls: Default::default() }
+    }
+
+    pub fn get_mock_dispatcher(&mut self) -> &mut MockDispatcher {
+        self.dispatcher.get_mut()
+    }
+
+    pub fn expect_convert_byte_array(
+        &mut self,
+        expected_array: jbyteArray,
+        out: Result<Vec<u8>, jni::errors::Error>,
+    ) {
+        self.expected_calls
+            .borrow_mut()
+            .push_back(ExpectedCall::ConvertByteArray { expected_array, out });
+    }
+
+    pub fn expect_get_array_length(
+        &mut self,
+        expected_array: jarray,
+        out: Result<jsize, jni::errors::Error>,
+    ) {
+        self.expected_calls
+            .borrow_mut()
+            .push_back(ExpectedCall::GetArrayLength { expected_array, out });
+    }
+
+    pub fn expect_get_short_array_region(
+        &mut self,
+        expected_array: jshortArray,
+        expected_start: jsize,
+        out: Result<Box<[jshort]>, jni::errors::Error>,
+    ) {
+        self.expected_calls.borrow_mut().push_back(ExpectedCall::GetShortArrayRegion {
+            expected_array,
+            expected_start,
+            out,
+        });
+    }
+
+    pub fn expect_get_int_array_region(
+        &mut self,
+        expected_array: jintArray,
+        expected_start: jsize,
+        out: Result<Box<[jint]>, jni::errors::Error>,
+    ) {
+        self.expected_calls.borrow_mut().push_back(ExpectedCall::GetIntArrayRegion {
+            expected_array,
+            expected_start,
+            out,
+        });
+    }
+}
+
+#[cfg(test)]
+impl<'a> Context<'a> for MockContext {
+    fn convert_byte_array(&self, array: jbyteArray) -> Result<Vec<u8>, jni::errors::Error> {
+        let mut expected_calls = self.expected_calls.borrow_mut();
+        match expected_calls.pop_front() {
+            Some(ExpectedCall::ConvertByteArray { expected_array, out })
+                if array == expected_array =>
+            {
+                out
+            }
+            Some(call) => {
+                expected_calls.push_front(call);
+                Err(jni::errors::Error::JniCall(jni::errors::JniError::Unknown))
+            }
+            None => Err(jni::errors::Error::JniCall(jni::errors::JniError::Unknown)),
+        }
+    }
+
+    fn get_array_length(&self, array: jarray) -> Result<jsize, jni::errors::Error> {
+        let mut expected_calls = self.expected_calls.borrow_mut();
+        match expected_calls.pop_front() {
+            Some(ExpectedCall::GetArrayLength { expected_array, out })
+                if array == expected_array =>
+            {
+                out
+            }
+            Some(call) => {
+                expected_calls.push_front(call);
+                Err(jni::errors::Error::JniCall(jni::errors::JniError::Unknown))
+            }
+            None => Err(jni::errors::Error::JniCall(jni::errors::JniError::Unknown)),
+        }
+    }
+
+    fn get_short_array_region(
+        &self,
+        array: jshortArray,
+        start: jsize,
+        buf: &mut [jshort],
+    ) -> Result<(), jni::errors::Error> {
+        let mut expected_calls = self.expected_calls.borrow_mut();
+        match expected_calls.pop_front() {
+            Some(ExpectedCall::GetShortArrayRegion { expected_array, expected_start, out })
+                if array == expected_array && start == expected_start =>
+            {
+                match out {
+                    Ok(expected_buf) => {
+                        buf.clone_from_slice(&expected_buf);
+                        Ok(())
+                    }
+                    Err(err) => Err(err),
+                }
+            }
+            Some(call) => {
+                expected_calls.push_front(call);
+                Err(jni::errors::Error::JniCall(jni::errors::JniError::Unknown))
+            }
+            None => Err(jni::errors::Error::JniCall(jni::errors::JniError::Unknown)),
+        }
+    }
+
+    fn get_int_array_region(
+        &self,
+        array: jintArray,
+        start: jsize,
+        buf: &mut [jint],
+    ) -> Result<(), jni::errors::Error> {
+        let mut expected_calls = self.expected_calls.borrow_mut();
+        match expected_calls.pop_front() {
+            Some(ExpectedCall::GetIntArrayRegion { expected_array, expected_start, out })
+                if array == expected_array && start == expected_start =>
+            {
+                match out {
+                    Ok(expected_buf) => {
+                        buf.clone_from_slice(&expected_buf);
+                        Ok(())
+                    }
+                    Err(err) => Err(err),
+                }
+            }
+            Some(call) => {
+                expected_calls.push_front(call);
+                Err(jni::errors::Error::JniCall(jni::errors::JniError::Unknown))
+            }
+            None => Err(jni::errors::Error::JniCall(jni::errors::JniError::Unknown)),
+        }
+    }
+
+    fn get_dispatcher(&self) -> Result<&'a mut dyn Dispatcher, UwbErr> {
+        unsafe { Ok(&mut *(self.dispatcher.as_ptr())) }
+    }
+}
+
+#[cfg(test)]
+enum ExpectedCall {
+    ConvertByteArray {
+        expected_array: jbyteArray,
+        out: Result<Vec<u8>, jni::errors::Error>,
+    },
+    GetArrayLength {
+        expected_array: jarray,
+        out: Result<jsize, jni::errors::Error>,
+    },
+    GetShortArrayRegion {
+        expected_array: jshortArray,
+        expected_start: jsize,
+        out: Result<Box<[jshort]>, jni::errors::Error>,
+    },
+    GetIntArrayRegion {
+        expected_array: jintArray,
+        expected_start: jsize,
+        out: Result<Box<[jint]>, jni::errors::Error>,
+    },
+}
diff --git a/service/uci/jni/rust/mock_dispatcher.rs b/service/uci/jni/rust/mock_dispatcher.rs
new file mode 100644
index 0000000..fe3f7e2
--- /dev/null
+++ b/service/uci/jni/rust/mock_dispatcher.rs
@@ -0,0 +1,101 @@
+use std::cell::RefCell;
+use std::collections::VecDeque;
+
+use uwb_uci_packets::GetDeviceInfoRspPacket;
+use uwb_uci_rust::error::UwbErr;
+use uwb_uci_rust::uci::{uci_hrcv::UciResponse, Dispatcher, JNICommand, Result};
+
+#[cfg(test)]
+#[derive(Default)]
+pub struct MockDispatcher {
+    expected_calls: RefCell<VecDeque<ExpectedCall>>,
+    device_info: Option<GetDeviceInfoRspPacket>,
+}
+
+#[cfg(test)]
+impl MockDispatcher {
+    pub fn new() -> Self {
+        Default::default()
+    }
+
+    pub fn expect_send_jni_command(&mut self, expected_cmd: JNICommand, out: Result<()>) {
+        self.expected_calls
+            .borrow_mut()
+            .push_back(ExpectedCall::SendJniCommand { expected_cmd, out })
+    }
+
+    pub fn expect_block_on_jni_command(
+        &mut self,
+        expected_cmd: JNICommand,
+        out: Result<UciResponse>,
+    ) {
+        self.expected_calls
+            .borrow_mut()
+            .push_back(ExpectedCall::BlockOnJniCommand { expected_cmd, out })
+    }
+
+    pub fn expect_wait_for_exit(&mut self, out: Result<()>) {
+        self.expected_calls.borrow_mut().push_back(ExpectedCall::WaitForExit { out })
+    }
+}
+
+#[cfg(test)]
+impl Drop for MockDispatcher {
+    fn drop(&mut self) {
+        assert!(self.expected_calls.borrow().is_empty());
+    }
+}
+
+#[cfg(test)]
+impl Dispatcher for MockDispatcher {
+    fn send_jni_command(&self, cmd: JNICommand) -> Result<()> {
+        let mut expected_calls = self.expected_calls.borrow_mut();
+        match expected_calls.pop_front() {
+            Some(ExpectedCall::SendJniCommand { expected_cmd, out }) if cmd == expected_cmd => out,
+            Some(call) => {
+                expected_calls.push_front(call);
+                Err(UwbErr::Undefined)
+            }
+            None => Err(UwbErr::Undefined),
+        }
+    }
+    fn block_on_jni_command(&self, cmd: JNICommand) -> Result<UciResponse> {
+        let mut expected_calls = self.expected_calls.borrow_mut();
+        match expected_calls.pop_front() {
+            Some(ExpectedCall::BlockOnJniCommand { expected_cmd, out }) if cmd == expected_cmd => {
+                out
+            }
+            Some(call) => {
+                expected_calls.push_front(call);
+                Err(UwbErr::Undefined)
+            }
+            None => Err(UwbErr::Undefined),
+        }
+    }
+    fn wait_for_exit(&mut self) -> Result<()> {
+        let mut expected_calls = self.expected_calls.borrow_mut();
+        match expected_calls.pop_front() {
+            Some(ExpectedCall::WaitForExit { out }) => out,
+            Some(call) => {
+                expected_calls.push_front(call);
+                Err(UwbErr::Undefined)
+            }
+            None => Err(UwbErr::Undefined),
+        }
+    }
+
+    fn set_device_info(&mut self, device_info: Option<GetDeviceInfoRspPacket>) {
+        self.device_info = device_info;
+    }
+
+    fn get_device_info(&self) -> &Option<GetDeviceInfoRspPacket> {
+        &self.device_info
+    }
+}
+
+#[cfg(test)]
+enum ExpectedCall {
+    SendJniCommand { expected_cmd: JNICommand, out: Result<()> },
+    BlockOnJniCommand { expected_cmd: JNICommand, out: Result<UciResponse> },
+    WaitForExit { out: Result<()> },
+}
diff --git a/service/uci/jni/utils/CondVar.cpp b/service/uci/jni/utils/CondVar.cpp
new file mode 100755
index 0000000..45e6ef5
--- /dev/null
+++ b/service/uci/jni/utils/CondVar.cpp
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * Copyright 2018 NXP.
+ *
+ * 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.
+ */
+
+/*
+ *  Encapsulate a condition variable for thread synchronization.
+ */
+
+#include "CondVar.h"
+
+#include <android-base/stringprintf.h>
+#include <android-base/logging.h>
+#include <string.h>
+
+#include "UwbJniUtil.h"
+
+using android::base::StringPrintf;
+
+/*******************************************************************************
+**
+** Function:        CondVar
+**
+** Description:     Initialize member variables.
+**
+** Returns:         None.
+**
+*******************************************************************************/
+CondVar::CondVar() {
+  pthread_condattr_t attr;
+  pthread_condattr_init(&attr);
+  pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);
+  memset(&mCondition, 0, sizeof(mCondition));
+  int const res = pthread_cond_init(&mCondition, &attr);
+  if (res) {
+    LOG(ERROR) << StringPrintf("CondVar::CondVar: fail init; error=0x%X", res);
+  }
+}
+
+/*******************************************************************************
+**
+** Function:        ~CondVar
+**
+** Description:     Cleanup all resources.
+**
+** Returns:         None.
+**
+*******************************************************************************/
+CondVar::~CondVar() {
+  int const res = pthread_cond_destroy(&mCondition);
+  if (res) {
+    LOG(ERROR) << StringPrintf("CondVar::~CondVar: fail destroy; error=0x%X",
+                               res);
+  }
+}
+
+/*******************************************************************************
+**
+** Function:        wait
+**
+** Description:     Block the caller and wait for a condition.
+**
+** Returns:         None.
+**
+*******************************************************************************/
+void CondVar::wait(Mutex &mutex) {
+  int const res = pthread_cond_wait(&mCondition, mutex.nativeHandle());
+  if (res) {
+    LOG(ERROR) << StringPrintf("CondVar::wait: fail wait; error=0x%X", res);
+  }
+}
+
+/*******************************************************************************
+**
+** Function:        wait
+**
+** Description:     Block the caller and wait for a condition.
+**                  millisec: Timeout in milliseconds.
+**
+** Returns:         True if wait is successful; false if timeout occurs.
+**
+*******************************************************************************/
+bool CondVar::wait(Mutex &mutex, long millisec) {
+  bool retVal = false;
+  struct timespec absoluteTime;
+
+  if (clock_gettime(CLOCK_MONOTONIC, &absoluteTime) == -1) {
+    LOG(ERROR) << StringPrintf("CondVar::wait: fail get time");
+  } else {
+    absoluteTime.tv_sec += millisec / 1000;
+    long ns = absoluteTime.tv_nsec + ((millisec % 1000) * 1000000);
+    if (ns > 1000000000) {
+      absoluteTime.tv_sec++;
+      absoluteTime.tv_nsec = ns - 1000000000;
+    } else
+      absoluteTime.tv_nsec = ns;
+  }
+
+  int waitResult =
+      pthread_cond_timedwait(&mCondition, mutex.nativeHandle(), &absoluteTime);
+  if ((waitResult != 0) && (waitResult != ETIMEDOUT))
+    LOG(ERROR) << StringPrintf("CondVar::wait: fail timed wait; error=0x%X",
+                               waitResult);
+  retVal = (waitResult == 0); // waited successfully
+  return retVal;
+}
+
+/*******************************************************************************
+**
+** Function:        notifyOne
+**
+** Description:     Unblock the waiting thread.
+**
+** Returns:         None.
+**
+*******************************************************************************/
+void CondVar::notifyOne() {
+  int const res = pthread_cond_signal(&mCondition);
+  if (res) {
+    LOG(ERROR) << StringPrintf("CondVar::notifyOne: fail signal; error=0x%X",
+                               res);
+  }
+}
diff --git a/service/uci/jni/utils/CondVar.h b/service/uci/jni/utils/CondVar.h
new file mode 100755
index 0000000..52a7490
--- /dev/null
+++ b/service/uci/jni/utils/CondVar.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * Copyright 2018 NXP.
+ *
+ * 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.
+ */
+
+/*
+ *  Encapsulate a condition variable for thread synchronization.
+ */
+
+#pragma once
+#include <pthread.h>
+
+#include "Mutex.h"
+
+class CondVar {
+public:
+  /*******************************************************************************
+  **
+  ** Function:        CondVar
+  **
+  ** Description:     Initialize member variables.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  CondVar();
+
+  /*******************************************************************************
+  **
+  ** Function:        ~CondVar
+  **
+  ** Description:     Cleanup all resources.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  ~CondVar();
+
+  /*******************************************************************************
+  **
+  ** Function:        wait
+  **
+  ** Description:     Block the caller and wait for a condition.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  void wait(Mutex &mutex);
+
+  /*******************************************************************************
+  **
+  ** Function:        wait
+  **
+  ** Description:     Block the caller and wait for a condition.
+  **                  millisec: Timeout in milliseconds.
+  **
+  ** Returns:         True if wait is successful; false if timeout occurs.
+  **
+  *******************************************************************************/
+  bool wait(Mutex &mutex, long millisec);
+
+  /*******************************************************************************
+  **
+  ** Function:        notifyOne
+  **
+  ** Description:     Unblock the waiting thread.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  void notifyOne();
+
+private:
+  pthread_cond_t mCondition;
+};
diff --git a/service/uci/jni/utils/IntervalTimer.cpp b/service/uci/jni/utils/IntervalTimer.cpp
new file mode 100755
index 0000000..3bb4fd4
--- /dev/null
+++ b/service/uci/jni/utils/IntervalTimer.cpp
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * Copyright 2018 NXP.
+ *
+ * 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.
+ */
+
+/*
+ *  Asynchronous interval timer.
+ */
+
+#include "IntervalTimer.h"
+
+#include <android-base/stringprintf.h>
+#include <android-base/logging.h>
+
+using android::base::StringPrintf;
+
+IntervalTimer::IntervalTimer() {
+  mTimerId = 0;
+  mCb = NULL;
+}
+
+bool IntervalTimer::set(int ms, TIMER_FUNC cb) {
+  if (mTimerId == 0) {
+    if (cb == NULL)
+      return false;
+
+    if (!create(cb))
+      return false;
+  }
+  if (cb != mCb) {
+    kill();
+    if (!create(cb))
+      return false;
+  }
+
+  int stat = 0;
+  struct itimerspec ts;
+  ts.it_value.tv_sec = ms / 1000;
+  ts.it_value.tv_nsec = (ms % 1000) * 1000000;
+
+  ts.it_interval.tv_sec = 0;
+  ts.it_interval.tv_nsec = 0;
+
+  stat = timer_settime(mTimerId, 0, &ts, 0);
+  if (stat == -1)
+    LOG(ERROR) << StringPrintf("fail set timer");
+  return stat == 0;
+}
+
+IntervalTimer::~IntervalTimer() { kill(); }
+
+void IntervalTimer::kill() {
+  if (mTimerId == 0)
+    return;
+
+  if (timer_delete(mTimerId) == -1)
+    LOG(ERROR) << StringPrintf("timer delete ERROR");
+  mTimerId = 0;
+  mCb = NULL;
+}
+
+bool IntervalTimer::create(TIMER_FUNC cb) {
+  static struct sigevent se;
+  int stat = 0;
+
+  /*
+   * Set the sigevent structure to cause the signal to be
+   * delivered by creating a new thread.
+   */
+  se.sigev_notify = SIGEV_THREAD;
+  se.sigev_value.sival_ptr = &mTimerId;
+  se.sigev_notify_function = cb;
+  se.sigev_notify_attributes = NULL;
+  mCb = cb;
+  stat = timer_create(CLOCK_MONOTONIC, &se, &mTimerId);
+  if (stat == -1)
+    LOG(ERROR) << StringPrintf("fail create timer");
+  return stat == 0;
+}
diff --git a/service/uci/jni/utils/IntervalTimer.h b/service/uci/jni/utils/IntervalTimer.h
new file mode 100755
index 0000000..c7dcd7e
--- /dev/null
+++ b/service/uci/jni/utils/IntervalTimer.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * Copyright 2018 NXP.
+ *
+ * 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.
+ */
+
+/*
+ *  Asynchronous interval timer.
+ */
+
+#include <time.h>
+
+class IntervalTimer {
+public:
+  typedef void (*TIMER_FUNC)(union sigval);
+
+  IntervalTimer();
+  ~IntervalTimer();
+  bool set(int ms, TIMER_FUNC cb);
+  void kill();
+  bool create(TIMER_FUNC);
+
+private:
+  timer_t mTimerId;
+  TIMER_FUNC mCb;
+};
diff --git a/service/uci/jni/utils/JniLog.h b/service/uci/jni/utils/JniLog.h
new file mode 100755
index 0000000..ac2a107
--- /dev/null
+++ b/service/uci/jni/utils/JniLog.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Copyright 2021 NXP.
+ *
+ * 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 _JNI_LOG_H_
+#define _JNI_LOG_H_
+
+#include <log/log.h>
+
+/* global log level Ref */
+extern bool uwb_debug_enabled;
+
+static const char *UWB_JNI_LOG = "UwbJni";
+
+#ifndef UNUSED
+#define UNUSED(X) (void)X;
+#endif
+
+/* define log module included when compile */
+#define ENABLE_JNI_LOGGING TRUE
+
+/* ############## Logging APIs of actual modules ################# */
+/* Logging APIs used by JNI module */
+#if (ENABLE_JNI_LOGGING == TRUE)
+#define JNI_TRACE_D(...)                                                       \
+  {                                                                            \
+    if (uwb_debug_enabled)                                                     \
+      LOG_PRI(ANDROID_LOG_DEBUG, UWB_JNI_LOG, __VA_ARGS__);                    \
+  }
+#define JNI_TRACE_I(...)                                                       \
+  {                                                                            \
+    if (uwb_debug_enabled)                                                     \
+      LOG_PRI(ANDROID_LOG_INFO, UWB_JNI_LOG, __VA_ARGS__);                     \
+  }
+#define JNI_TRACE_W(...)                                                       \
+  {                                                                            \
+    if (uwb_debug_enabled)                                                     \
+      LOG_PRI(ANDROID_LOG_WARN, UWB_JNI_LOG, __VA_ARGS__);                     \
+  }
+#define JNI_TRACE_E(...)                                                       \
+  {                                                                            \
+    if (uwb_debug_enabled)                                                     \
+      LOG_PRI(ANDROID_LOG_ERROR, UWB_JNI_LOG, __VA_ARGS__);                    \
+  }
+#else
+#define JNI_TRACE_D(...)
+#define JNI_TRACE_I(...)
+#define JNI_TRACE_W(...)
+#define JNI_TRACE_E(...)
+#endif /* Logging APIs used by JNI module */
+
+#endif /* _JNI_LOG_H_*/
diff --git a/service/uci/jni/utils/Mutex.cpp b/service/uci/jni/utils/Mutex.cpp
new file mode 100755
index 0000000..e2609f0
--- /dev/null
+++ b/service/uci/jni/utils/Mutex.cpp
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * Copyright 2018 NXP.
+ *
+ * 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.
+ */
+
+/*
+ *  Encapsulate a mutex for thread synchronization.
+ */
+
+#include "Mutex.h"
+
+#include <android-base/stringprintf.h>
+#include <android-base/logging.h>
+#include <string.h>
+
+#include "UwbJniUtil.h"
+
+using android::base::StringPrintf;
+
+/*******************************************************************************
+**
+** Function:        Mutex
+**
+** Description:     Initialize member variables.
+**
+** Returns:         None.
+**
+*******************************************************************************/
+Mutex::Mutex() {
+  memset(&mMutex, 0, sizeof(mMutex));
+  int res = pthread_mutex_init(&mMutex, NULL);
+  if (res != 0) {
+    LOG(ERROR) << StringPrintf("Mutex::Mutex: fail init; error=0x%X", res);
+  }
+}
+
+/*******************************************************************************
+**
+** Function:        ~Mutex
+**
+** Description:     Cleanup all resources.
+**
+** Returns:         None.
+**
+*******************************************************************************/
+Mutex::~Mutex() {
+  int res = pthread_mutex_destroy(&mMutex);
+  if (res != 0) {
+    LOG(ERROR) << StringPrintf("Mutex::~Mutex: fail destroy; error=0x%X", res);
+  }
+}
+
+/*******************************************************************************
+**
+** Function:        lock
+**
+** Description:     Block the thread and try lock the mutex.
+**
+** Returns:         None.
+**
+*******************************************************************************/
+void Mutex::lock() {
+  int res = pthread_mutex_lock(&mMutex);
+  if (res != 0) {
+    LOG(ERROR) << StringPrintf("Mutex::lock: fail lock; error=0x%X", res);
+  }
+}
+
+/*******************************************************************************
+**
+** Function:        unlock
+**
+** Description:     Unlock a mutex to unblock a thread.
+**
+** Returns:         None.
+**
+*******************************************************************************/
+void Mutex::unlock() {
+  int res = pthread_mutex_unlock(&mMutex);
+  if (res != 0) {
+    LOG(ERROR) << StringPrintf("Mutex::unlock: fail unlock; error=0x%X", res);
+  }
+}
+
+/*******************************************************************************
+**
+** Function:        tryLock
+**
+** Description:     Try to lock the mutex.
+**
+** Returns:         True if the mutex is locked.
+**
+*******************************************************************************/
+bool Mutex::tryLock() {
+  int res = pthread_mutex_trylock(&mMutex);
+  if ((res != 0) && (res != EBUSY)) {
+    LOG(ERROR) << StringPrintf("Mutex::tryLock: error=0x%X", res);
+  }
+  return res == 0;
+}
+
+/*******************************************************************************
+**
+** Function:        nativeHandle
+**
+** Description:     Get the handle of the mutex.
+**
+** Returns:         Handle of the mutex.
+**
+*******************************************************************************/
+pthread_mutex_t *Mutex::nativeHandle() { return &mMutex; }
diff --git a/service/uci/jni/utils/Mutex.h b/service/uci/jni/utils/Mutex.h
new file mode 100755
index 0000000..86fa563
--- /dev/null
+++ b/service/uci/jni/utils/Mutex.h
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * Copyright 2018 NXP.
+ *
+ * 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.
+ */
+
+/*
+ *  Encapsulate a mutex for thread synchronization.
+ */
+
+#pragma once
+#include <pthread.h>
+
+class Mutex {
+public:
+  /*******************************************************************************
+  **
+  ** Function:        Mutex
+  **
+  ** Description:     Initialize member variables.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  Mutex();
+
+  /*******************************************************************************
+  **
+  ** Function:        ~Mutex
+  **
+  ** Description:     Cleanup all resources.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  ~Mutex();
+
+  /*******************************************************************************
+  **
+  ** Function:        lock
+  **
+  ** Description:     Block the thread and try lock the mutex.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  void lock();
+
+  /*******************************************************************************
+  **
+  ** Function:        unlock
+  **
+  ** Description:     Unlock a mutex to unblock a thread.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  void unlock();
+
+  /*******************************************************************************
+  **
+  ** Function:        tryLock
+  **
+  ** Description:     Try to lock the mutex.
+  **
+  ** Returns:         True if the mutex is locked.
+  **
+  *******************************************************************************/
+  bool tryLock();
+
+  /*******************************************************************************
+  **
+  ** Function:        nativeHandle
+  **
+  ** Description:     Get the handle of the mutex.
+  **
+  ** Returns:         Handle of the mutex.
+  **
+  *******************************************************************************/
+  pthread_mutex_t *nativeHandle();
+
+  class Autolock {
+  public:
+    inline Autolock(Mutex &mutex) : mLock(mutex) { mLock.lock(); }
+    inline Autolock(Mutex *mutex) : mLock(*mutex) { mLock.lock(); }
+    inline ~Autolock() { mLock.unlock(); }
+
+  private:
+    Mutex &mLock;
+  };
+
+private:
+  pthread_mutex_t mMutex;
+};
+
+typedef Mutex::Autolock AutoMutex;
diff --git a/service/uci/jni/utils/ScopedJniEnv.h b/service/uci/jni/utils/ScopedJniEnv.h
new file mode 100755
index 0000000..77ddc7a
--- /dev/null
+++ b/service/uci/jni/utils/ScopedJniEnv.h
@@ -0,0 +1,62 @@
+/*****************************************************************
+//Copyright 2017 Google Inc. All Rights Reserved.
+
+****************************************************************/
+
+#ifndef _UWB_JNI_SCOPEDJNIENV_H_
+#define _UWB_JNI_SCOPEDJNIENV_H_
+
+#include <jni.h>
+
+class ScopedJniEnv {
+public:
+  ScopedJniEnv(JavaVM *jvm) : jvm_(jvm), env_(NULL), is_attached_(false) {
+    // We don't make any assumptions about the state of the current thread, and
+    // we want to leave it in the state we received it with respect to the
+    // JavaVm. So we only attach and detach when needed, and we always delete
+    // local references.
+    jint error =
+        jvm_->GetEnv(reinterpret_cast<void **>(&env_), JNI_VERSION_1_2);
+    if (error != JNI_OK) {
+      jvm_->AttachCurrentThread(&env_, NULL);
+      is_attached_ = true;
+    }
+    if (env_ != NULL) {
+      env_->PushLocalFrame(0);
+    }
+  }
+
+  virtual ~ScopedJniEnv() {
+    if (env_ != NULL) {
+      env_->PopLocalFrame(NULL);
+      if (is_attached_) {
+        // A return value indicating possible errors is available here.
+        (void)jvm_->DetachCurrentThread();
+      }
+    }
+  }
+
+  operator JNIEnv *() { return env_; }
+
+  JNIEnv *operator->() { return env_; }
+
+  bool isValid() const { return env_ != NULL; }
+
+private:
+#if __cplusplus >= 201103L
+
+  ScopedJniEnv(const ScopedJniEnv &) = delete;
+
+  void operator=(const ScopedJniEnv &) = delete;
+
+#else
+  ScopedJniEnv(const ScopedJniEnv &);
+  void operator=(const ScopedJniEnv &);
+#endif
+
+  JavaVM *jvm_;
+  JNIEnv *env_;
+  bool is_attached_;
+};
+
+#endif
diff --git a/service/uci/jni/utils/SyncEvent.cpp b/service/uci/jni/utils/SyncEvent.cpp
new file mode 100755
index 0000000..e9907e8
--- /dev/null
+++ b/service/uci/jni/utils/SyncEvent.cpp
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * Copyright 2021 NXP.
+ *
+ * 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 "SyncEvent.h"
+
+#include <android-base/stringprintf.h>
+#include <android-base/logging.h>
+
+using android::base::StringPrintf;
+
+std::list<SyncEvent *> syncEventList;
+std::mutex syncEventListMutex;
+
+SyncEvent::~SyncEvent() { mWait = false; }
+
+void SyncEvent::start() {
+  mWait = false;
+  mMutex.lock();
+}
+
+void SyncEvent::wait() {
+  mWait = true;
+  addEvent();
+  while (mWait) {
+    mCondVar.wait(mMutex);
+  }
+}
+
+bool SyncEvent::wait(long millisec) {
+  bool retVal;
+  mWait = true;
+  addEvent();
+  while (mWait) {
+    retVal = mCondVar.wait(mMutex, millisec);
+    if (!retVal)
+      mWait = false;
+  }
+  return retVal;
+}
+
+void SyncEvent::notifyOne() {
+  mWait = false;
+  removeEvent();
+  mCondVar.notifyOne();
+}
+
+void SyncEvent::notify() {
+  mWait = false;
+  mCondVar.notifyOne();
+}
+
+void SyncEvent::end() {
+  mWait = false;
+  mMutex.unlock();
+}
+
+void SyncEvent::addEvent() {
+  std::lock_guard<std::mutex> guard(
+      syncEventListMutex); // with lock access list
+  bool contains = (std::find(syncEventList.begin(), syncEventList.end(),
+                             this) != syncEventList.end());
+  if (!contains)
+    syncEventList.push_back(this);
+}
+
+void SyncEvent::removeEvent() {
+  std::lock_guard<std::mutex> guard(
+      syncEventListMutex); // with lock access list
+  syncEventList.remove(this);
+}
+
+void SyncEvent::notifyAll() {
+  std::lock_guard<std::mutex> guard(
+      syncEventListMutex); // with lock access list
+  for (auto &i : syncEventList) {
+    if (i != NULL)
+      i->notify();
+  }
+  syncEventList.clear();
+}
diff --git a/service/uci/jni/utils/SyncEvent.h b/service/uci/jni/utils/SyncEvent.h
new file mode 100755
index 0000000..45ae85b
--- /dev/null
+++ b/service/uci/jni/utils/SyncEvent.h
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * Copyright 2019-2020 NXP.
+ *
+ * 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.
+ */
+
+/*
+ *  Synchronize two or more threads using a condition variable and a mutex.
+ */
+#pragma once
+#include <list>
+
+#include "CondVar.h"
+#include "Mutex.h"
+using namespace std;
+
+class SyncEvent;
+
+extern std::list<SyncEvent *> syncEventList;
+
+class SyncEvent {
+public:
+  /*******************************************************************************
+  **
+  ** Function:        ~SyncEvent
+  **
+  ** Description:     Cleanup all resources.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  ~SyncEvent();
+
+  /*******************************************************************************
+  **
+  ** Function:        start
+  **
+  ** Description:     Start a synchronization operation.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  void start();
+
+  /*******************************************************************************
+  **
+  ** Function:        wait
+  **
+  ** Description:     Block the thread and wait for the event to occur.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  void wait();
+
+  /*******************************************************************************
+  **
+  ** Function:        wait
+  **
+  ** Description:     Block the thread and wait for the event to occur.
+  **                  millisec: Timeout in milliseconds.
+  **
+  ** Returns:         True if wait is successful; false if timeout occurs.
+  **
+  *******************************************************************************/
+  bool wait(long millisec);
+
+  /*******************************************************************************
+  **
+  ** Function:        notifyOne
+  **
+  ** Description:     Notify a blocked thread that the event has occurred.
+  *Unblocks it.
+  **                  Deregisters cached event.
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  void notifyOne();
+
+  /*******************************************************************************
+  **
+  ** Function:        notify
+  **
+  ** Description:     Notify a blocked thread that the event has occurred.
+  *Unblocks it.
+  **                  This function won't deregister cached event
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  void notify();
+
+  /*******************************************************************************
+  **
+  ** Function:        end
+  **
+  ** Description:     End a synchronization operation.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  void end();
+
+  /********Implement equality operator for SyncEvent
+   * Class***********************/
+  bool operator==(const SyncEvent &event) { return (this == &event); }
+
+  /*******************************************************************************
+  **
+  ** Function:        addEvent
+  **
+  ** Description:     cache event locally
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  void addEvent();
+
+  /*******************************************************************************
+  **
+  ** Function:        notifyAll
+  **
+  ** Description:     Notify all blocked thread that the event has occurred.
+  *Unblocks it.
+  **                  clears the event cache
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  void notifyAll();
+
+  /*******************************************************************************
+  **
+  ** Function:        removeEvent
+  **
+  ** Description:     remove event from cache event.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  void removeEvent();
+
+private:
+  CondVar mCondVar;
+  Mutex mMutex;
+  bool mWait = false;
+};
+
+/*****************************************************************************/
+/*****************************************************************************/
+
+/*****************************************************************************
+**
+**  Name:           SyncEventGuard
+**
+**  Description:    Automatically start and end a synchronization event.
+**
+*****************************************************************************/
+class SyncEventGuard {
+public:
+  /*******************************************************************************
+  **
+  ** Function:        SyncEventGuard
+  **
+  ** Description:     Start a synchronization operation.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  SyncEventGuard(SyncEvent &event) : mEvent(event) {
+    event.start(); // automatically start operation
+  };
+
+  /*******************************************************************************
+  **
+  ** Function:        ~SyncEventGuard
+  **
+  ** Description:     End a synchronization operation.
+  **
+  ** Returns:         None.
+  **
+  *******************************************************************************/
+  ~SyncEventGuard() {
+    mEvent.end(); // automatically end operation
+  };
+
+private:
+  SyncEvent &mEvent;
+};
diff --git a/service/uci/jni/utils/UwbJniUtil.cpp b/service/uci/jni/utils/UwbJniUtil.cpp
new file mode 100755
index 0000000..94ee987
--- /dev/null
+++ b/service/uci/jni/utils/UwbJniUtil.cpp
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * Copyright 2018-2020 NXP.
+ *
+ * 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 "UwbJniUtil.h"
+
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+
+#include "JniLog.h"
+#include "UwbJniInternal.h"
+
+/*******************************************************************************
+**
+** Function:        JNI_OnLoad
+**
+** Description:     Register all JNI functions with Java Virtual Machine.
+**                  jvm: Java Virtual Machine.
+**                  reserved: Not used.
+**
+** Returns:         JNI version.
+**
+*******************************************************************************/
+jint JNI_OnLoad(JavaVM *jvm, void *) {
+  JNI_TRACE_I("%s: enter", __func__);
+  JNIEnv *env = NULL;
+
+  JNI_TRACE_I("UWB Service: loading uci JNI");
+
+  // Check JNI version
+  if (jvm->GetEnv((void **)&env, JNI_VERSION_1_6))
+    return JNI_ERR;
+
+  if (android::register_com_android_uwb_dhimpl_UwbNativeManager(env) == -1)
+    return JNI_ERR;
+  /*if (android::register_com_android_uwb_dhimpl_UwbRfTestNativeManager(env) ==
+      -1)
+    return JNI_ERR;*/
+
+  JNI_TRACE_I("%s: exit", __func__);
+  return JNI_VERSION_1_6;
+}
+
+/*******************************************************************************
+**
+** Function:        uwb_jni_cache_jclass
+**
+** Description:     This API invoked during JNI initialization to register
+**                  Required class and corresponding Global refference will be
+**                  used during sending Ranging ntf to upper layer.
+**
+** Returns:         Status code.
+**
+*******************************************************************************/
+int uwb_jni_cache_jclass(JNIEnv *env, const char *className,
+                         jclass *cachedJclass) {
+  jclass cls = env->FindClass(className);
+  if (cls == NULL) {
+    JNI_TRACE_E("%s: find class error", __func__);
+    return -1;
+  }
+
+  *cachedJclass = static_cast<jclass>(env->NewGlobalRef(cls));
+  if (*cachedJclass == NULL) {
+    JNI_TRACE_E("%s: global ref error", __func__);
+    return -1;
+  }
+  return 0;
+}
\ No newline at end of file
diff --git a/service/uci/jni/utils/UwbJniUtil.h b/service/uci/jni/utils/UwbJniUtil.h
new file mode 100755
index 0000000..52be662
--- /dev/null
+++ b/service/uci/jni/utils/UwbJniUtil.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * Copyright 2018-2020 NXP.
+ *
+ * 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.
+ */
+#pragma once
+#include <jni.h>
+#include <pthread.h>
+#include <semaphore.h>
+#include <sys/queue.h>
+
+#define JNI_NULL 0
+
+struct uwb_jni_native_data {
+  /* Our VM */
+  JavaVM *vm;
+  jobject manager;
+  jclass mRangeDataClass;
+  jclass rangingTwoWayMeasuresClass;
+  jclass mRangeTdoaMeasuresClass;
+  jclass periodicTxDataClass;
+  jclass perRxDataClass;
+  jclass uwbLoopBackDataClass;
+  jclass multicastUpdateListDataClass;
+};
+
+jint JNI_OnLoad(JavaVM *jvm, void *reserved);
+
+int uwb_jni_cache_jclass(JNIEnv *env, const char *clsname,
+                         jclass *cached_jclass);
+
+namespace android {
+int register_com_android_uwb_dhimpl_UwbNativeManager(JNIEnv *env);
+int register_com_android_uwb_dhimpl_NxpUwbNativeManager(JNIEnv *env);
+int register_com_android_uwb_dhimpl_UwbRfTestNativeManager(JNIEnv *env);
+} // namespace android
\ No newline at end of file
diff --git a/service/uci/jni/uwb_rust_test_config_template.xml b/service/uci/jni/uwb_rust_test_config_template.xml
new file mode 100644
index 0000000..f93397c
--- /dev/null
+++ b/service/uci/jni/uwb_rust_test_config_template.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Configuration for {MODULE} Rust tests">
+   <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+   <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+       <option name="cleanup" value="true" />
+       <option name="push" value="{MODULE}->/data/local/tmp/{MODULE}" />
+   </target_preparer>
+   <test class="com.android.tradefed.testtype.rust.RustBinaryTest" >
+       <option name="test-device-path" value="/data/local/tmp" />
+       <option name="module-name" value="{MODULE}" />
+   </test>
+   <object type="module_controller"
+           class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+       <option name="mainline-module-package-name" value="com.google.android.uwb" />
+   </object>
+</configuration>