[automerger skipped] Bump Mainline Module Version Codes in AOSP to 339990000 am: 5d2774b777 am: 4398917053 -s ours
am skip reason: Merged-In Idf5c494b2933967daf1406490c87053ee2c573d8 with SHA-1 cb6e6fb4af is already in history
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Uwb/+/2092273
Change-Id: I8533279e735b4d9c8522a2c06e44c5bfe3f4574f
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
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..ef8be34 100644
--- a/apex/apex_manifest.json
+++ b/apex/apex_manifest.json
@@ -1,4 +1,5 @@
{
"name": "com.android.uwb",
- "version": 339990000
+ "version": 330090000
}
+
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..c199cd5
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbServiceCore.java
@@ -0,0 +1,642 @@
+/*
+ * 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 GenericSpecificationParams mSpecificationParams;
+ 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) {
+ // Clear the cached capabilities on country code changes.
+ Log.v(TAG, "Clearing cached specification params on country code change");
+ mSpecificationParams = null;
+ }
+
+ 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;
+ }
+
+ private void updateSpecificationInfo() {
+ 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;
+ }
+ mSpecificationParams = specificationParams.second;
+ }
+
+ public PersistableBundle getSpecificationInfo() {
+ if (mSpecificationParams == null) {
+ updateSpecificationInfo();
+ }
+ if (mSpecificationParams == null) return new PersistableBundle();
+ return mSpecificationParams.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..29a79fb
--- /dev/null
+++ b/service/java/com/android/server/uwb/UwbSessionManager.java
@@ -0,0 +1,1042 @@
+/*
+ * 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..c17e91c
--- /dev/null
+++ b/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java
@@ -0,0 +1,687 @@
+/*
+ * 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();
+ }
+
+ @Test
+ public void testGetSpecificationInfoUsesCache() throws Exception {
+ verifyGetSpecificationInfoSuccess();
+ clearInvocations(mUwbConfigurationManager);
+
+ PersistableBundle specifications = mUwbServiceCore.getSpecificationInfo();
+ assertThat(specifications).isNotNull();
+
+ verifyNoMoreInteractions(mUwbConfigurationManager);
+ }
+
+ 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", ¶).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>