diff options
85 files changed, 4193 insertions, 508 deletions
diff --git a/framework/jarjar-rules.txt b/framework/jarjar-rules.txt index 375d9c6b..589e47b2 100644 --- a/framework/jarjar-rules.txt +++ b/framework/jarjar-rules.txt @@ -1,6 +1,8 @@ ## used by service-uwb ## # Statically included annotations. rule androidx.annotation.** com.android.x.uwb.@0 +# Statically linked cbor library +rule co.nstant.in.cbor.** com.android.x.uwb.@0 # Statically included module utils. rule com.android.modules.utils.** com.android.x.uwb.@0 # Statically included HAL stubs. @@ -12,5 +14,7 @@ rule com.google.uwb.** com.android.x.uwb.@0 rule com.android.internal.util.** com.android.x.uwb.@0 # Use our statically linked protobuf library rule com.google.protobuf.** com.android.x.uwb.@0 +# Statically linked bouncy castle library +rule org.bouncycastle.** com.android.x.uwb.@0 ## used by both framework-uwb and service-uwb ## diff --git a/framework/java/android/uwb/UwbManager.java b/framework/java/android/uwb/UwbManager.java index 1b22c2e3..154b9fbd 100644 --- a/framework/java/android/uwb/UwbManager.java +++ b/framework/java/android/uwb/UwbManager.java @@ -382,7 +382,7 @@ public final class UwbManager { @NonNull RangingReport rangingReport); /** - * Invoked when requesting a check on pointed target. + * Invoked to check pointed target decision by Oem. * * @param pointedTargetBundle pointed target params * @return Oem pointed status @@ -981,13 +981,15 @@ public final class UwbManager { public static final int MESSAGE_TYPE_COMMAND = 1; /** * @hide - * Message Type value reserved for testing. + * Message Type for C-APDU (Command - Application Protocol Data Unit), + * used for communication with secure component. */ public static final int MESSAGE_TYPE_TEST_1 = 4; /** * @hide - * Message Type value reserved for testing. + * Message Type for R-APDU (Response - Application Protocol Data Unit), + * used for communication with secure component. */ public static final int MESSAGE_TYPE_TEST_2 = 5; diff --git a/indev_uwb_adaptation/jni/src/api.rs b/indev_uwb_adaptation/jni/src/api.rs index d7e6150f..1c1d7dd0 100644 --- a/indev_uwb_adaptation/jni/src/api.rs +++ b/indev_uwb_adaptation/jni/src/api.rs @@ -17,8 +17,8 @@ //! Internally after the UWB core service is instantiated, the pointer to the service is saved //! on the calling Java side. use jni::objects::{GlobalRef, JObject, JValue}; -use jni::signature::JavaType; -use jni::sys::{jboolean, jbyte, jbyteArray, jint, jlong, jobject}; +use jni::signature::ReturnType; +use jni::sys::{jboolean, jbyte, jbyteArray, jint, jlong, jobject, jvalue}; use jni::JNIEnv; use log::{debug, error}; use num_traits::FromPrimitive; @@ -425,7 +425,7 @@ fn get_power_stats(ctx: JniContext) -> Result<jobject> { let power_stats = uwb_service.android_get_power_stats()?; let ps_jni = PowerStatsJni::try_from(PowerStatsWithEnv::new(ctx.env, power_stats))?; - Ok(ps_jni.jni_context.obj.into_inner()) + Ok(ps_jni.jni_context.obj.into_raw()) } fn get_uwb_service(ctx: JniContext) -> Result<&mut UwbServiceWrapper> { @@ -471,8 +471,8 @@ fn get_class_loader_obj(env: &JNIEnv) -> Result<GlobalRef> { let class_loader = env.call_method_unchecked( uwb_service_core_class, get_class_loader_method, - JavaType::Object("java/lang/ClassLoader".into()), - &[JValue::Void], + ReturnType::Object, + &[jvalue::from(JValue::Void)], )?; let class_loader_jobject = class_loader.l()?; Ok(env.new_global_ref(class_loader_jobject)?) diff --git a/indev_uwb_adaptation/jni/src/callback.rs b/indev_uwb_adaptation/jni/src/callback.rs index 706af6fe..f0b974ba 100644 --- a/indev_uwb_adaptation/jni/src/callback.rs +++ b/indev_uwb_adaptation/jni/src/callback.rs @@ -20,6 +20,7 @@ use std::collections::HashMap; use jni::objects::{GlobalRef, JClass, JMethodID, JObject, JValue}; use jni::signature::TypeSignature; +use jni::sys::jvalue; use jni::{AttachGuard, JavaVM}; use log::error; @@ -57,7 +58,7 @@ pub struct UwbServiceCallbackImpl { env: AttachGuard<'static>, class_loader_obj: GlobalRef, callback_obj: GlobalRef, - jmethod_id_map: HashMap<String, JMethodID<'static>>, + jmethod_id_map: HashMap<String, JMethodID>, jclass_map: HashMap<String, GlobalRef>, } @@ -77,6 +78,8 @@ impl UwbServiceCallbackImpl { } fn find_local_class(&mut self, class_name: &str) -> Result<GlobalRef> { + let class_name_jobject = *self.env.new_string(class_name)?; + let jclass = match self.jclass_map.get(class_name) { Some(jclass) => jclass.clone(), None => { @@ -86,7 +89,7 @@ impl UwbServiceCallbackImpl { self.class_loader_obj.as_obj(), "findClass", "(Ljava/lang/String;)Ljava/lang/Class;", - &[JValue::Object(JObject::from(self.env.new_string(class_name)?))], + &[JValue::Object(class_name_jobject)], )? .l()?; @@ -99,7 +102,7 @@ impl UwbServiceCallbackImpl { Ok(jclass) } - fn cached_jni_call(&mut self, name: &str, sig: &str, args: &[JValue]) -> Result<()> { + fn cached_jni_call(&mut self, name: &str, sig: &str, args: &[jvalue]) -> Result<()> { let type_signature = TypeSignature::from_str(sig)?; if type_signature.args.len() != args.len() { return Err(Error::Jni(jni::errors::Error::InvalidArgList(type_signature))); @@ -126,8 +129,11 @@ impl UwbServiceCallbackImpl { impl UwbServiceCallback for UwbServiceCallbackImpl { fn on_service_reset(&mut self, success: bool) { - let result = - self.cached_jni_call("onServiceResetReceived", "(Z)V", &[JValue::Bool(success as u8)]); + let result = self.cached_jni_call( + "onServiceResetReceived", + "(Z)V", + &[jvalue::from(JValue::Bool(success as u8))], + ); result_helper("on_service_reset", result); } @@ -135,7 +141,7 @@ impl UwbServiceCallback for UwbServiceCallbackImpl { let result = self.cached_jni_call( "onDeviceStatusNotificationReceived", "(I)V", - &[JValue::Int(state as i32)], + &[jvalue::from(JValue::Int(state as i32))], ); result_helper("on_uci_device_status_changed", result); } @@ -150,9 +156,9 @@ impl UwbServiceCallback for UwbServiceCallbackImpl { "onSessionStatusNotificationReceived", "(JII)V", &[ - JValue::Long(session_id as i64), - JValue::Int(session_state as i32), - JValue::Int(reason_code as i32), + jvalue::from(JValue::Long(session_id as i64)), + jvalue::from(JValue::Int(session_state as i32)), + jvalue::from(JValue::Int(reason_code as i32)), ], ); result_helper("on_session_state_changed", result); @@ -198,7 +204,10 @@ impl UwbServiceCallback for UwbServiceCallbackImpl { let result = self.cached_jni_call( "onRangeDataNotificationReceived", "(JLcom/android/server/uwb/data/UwbRangingData;)V", - &[JValue::Long(session_id as i64), JValue::Object(uwb_raning_data_jobject)], + &[ + jvalue::from(JValue::Long(session_id as i64)), + jvalue::from(JValue::Object(uwb_raning_data_jobject)), + ], ); result_helper("on_range_data_received", result); } @@ -218,13 +227,17 @@ impl UwbServiceCallback for UwbServiceCallbackImpl { error!("UWB Service Callback: Failed to set byte array: {:?}", err); return; } + + // Safety: payload_jbyte_array safely instantiated above. + let payload_jobject = unsafe { JObject::from_raw(payload_jbyte_array) }; + let result = self.cached_jni_call( "onVendorUciNotificationReceived", "(II[B)V", &[ - JValue::Int(gid as i32), - JValue::Int(oid as i32), - JValue::Object(JObject::from(payload_jbyte_array)), + jvalue::from(JValue::Int(gid as i32)), + jvalue::from(JValue::Int(oid as i32)), + jvalue::from(JValue::Object(payload_jobject)), ], ); diff --git a/indev_uwb_adaptation/jni/src/context.rs b/indev_uwb_adaptation/jni/src/context.rs index 97f9b1ec..315eba7c 100644 --- a/indev_uwb_adaptation/jni/src/context.rs +++ b/indev_uwb_adaptation/jni/src/context.rs @@ -43,7 +43,7 @@ impl<'a> JniContext<'a> { pub fn byte_arr_getter(&self, method: &str) -> Result<Vec<u8>, jni::errors::Error> { let val_obj = self.env.call_method(self.obj, method, "()[B", &[])?.l()?; - self.env.convert_byte_array(val_obj.into_inner()) + self.env.convert_byte_array(val_obj.into_raw()) } pub fn object_getter( diff --git a/indev_uwb_adaptation/jni/src/object_mapping.rs b/indev_uwb_adaptation/jni/src/object_mapping.rs index e6dcc8d9..e7b21322 100644 --- a/indev_uwb_adaptation/jni/src/object_mapping.rs +++ b/indev_uwb_adaptation/jni/src/object_mapping.rs @@ -370,10 +370,10 @@ impl<'a> FiraControleeParamsJni<'a> { let env = self.jni_context.env; let addr_arr = self.jni_context.object_getter("getAddressList", "[android/uwb/UwbAddress;")?; - let addr_len = env.get_array_length(addr_arr.into_inner())?; + let addr_len = env.get_array_length(addr_arr.into_raw())?; let subs_arr = self.jni_context.object_getter("getSubSessionIdList", "[I")?; - let subs_len = env.get_array_length(subs_arr.into_inner())?; + let subs_len = env.get_array_length(subs_arr.into_raw())?; if addr_len != subs_len { return Err(Error::Parse(format!( @@ -386,10 +386,10 @@ impl<'a> FiraControleeParamsJni<'a> { let size: usize = addr_len.try_into().unwrap(); let mut subs_arr_vec = vec![0i32; size]; - env.get_int_array_region(subs_arr.into_inner(), 0, &mut subs_arr_vec)?; + env.get_int_array_region(subs_arr.into_raw(), 0, &mut subs_arr_vec)?; for (i, sub_session) in subs_arr_vec.iter().enumerate() { - let uwb_address_obj = env.get_object_array_element(addr_arr.into_inner(), i as i32)?; + let uwb_address_obj = env.get_object_array_element(addr_arr.into_raw(), i as i32)?; let uwb_address: u16 = UwbAddressJni::new(env, uwb_address_obj).try_into()?; controlees .push(Controlee { short_address: uwb_address, subsession_id: *sub_session as u32 }); @@ -495,27 +495,27 @@ pub struct UwbRangingDataJni<'a> { impl<'a> TryFrom<SessionRangeDataWithEnv<'a>> for UwbRangingDataJni<'a> { type Error = Error; fn try_from(data_obj: SessionRangeDataWithEnv<'a>) -> Result<Self> { - let (mac_address_indicator, measurements_size) = match data_obj - .session_range_data - .ranging_measurements - { - RangingMeasurements::ShortAddressTwoWay(ref m) => { - (MacAddressIndicator::ShortAddress, m.len()) - } - RangingMeasurements::ExtendedAddressTwoWay(ref m) => { - (MacAddressIndicator::ExtendedAddress, m.len()) - } - RangingMeasurements::ShortDltdoa(ref m) => (MacAddressIndicator::ShortAddress, m.len()), - RangingMeasurements::ExtendedDltdoa(ref m) => { - (MacAddressIndicator::ExtendedAddress, m.len()) - } - RangingMeasurements::ShortAddressOwrAoa(ref m) => { - (MacAddressIndicator::ShortAddress, m.len()) - } - RangingMeasurements::ExtendedAddressOwrAoa(ref m) => { - (MacAddressIndicator::ExtendedAddress, m.len()) - } - }; + let (mac_address_indicator, measurements_size) = + match data_obj.session_range_data.ranging_measurements { + RangingMeasurements::ShortAddressTwoWay(ref m) => { + (MacAddressIndicator::ShortAddress, m.len()) + } + RangingMeasurements::ExtendedAddressTwoWay(ref m) => { + (MacAddressIndicator::ExtendedAddress, m.len()) + } + RangingMeasurements::ShortAddressDltdoa(ref m) => { + (MacAddressIndicator::ShortAddress, m.len()) + } + RangingMeasurements::ExtendedAddressDltdoa(ref m) => { + (MacAddressIndicator::ExtendedAddress, m.len()) + } + RangingMeasurements::ShortAddressOwrAoa(ref m) => { + (MacAddressIndicator::ShortAddress, m.len()) + } + RangingMeasurements::ExtendedAddressOwrAoa(ref m) => { + (MacAddressIndicator::ExtendedAddress, m.len()) + } + }; let measurements_jni = UwbTwoWayMeasurementJni::try_from(RangingMeasurementsWithEnv::new( data_obj.env, data_obj.uwb_two_way_measurement_jclass, @@ -524,6 +524,9 @@ impl<'a> TryFrom<SessionRangeDataWithEnv<'a>> for UwbRangingDataJni<'a> { let raw_notification_jbytearray = data_obj.env.byte_array_from_slice(&data_obj.session_range_data.raw_ranging_data)?; // TODO(b/246678053): Check on using OwrAoa measurement class here. + + // Safety: raw_notification_jbytearray safely instantiated above. + let raw_notification_jobject = unsafe { JObject::from_raw(raw_notification_jbytearray) }; let ranging_data_jni = data_obj.env.new_object( data_obj.uwb_ranging_data_jclass, "(JJIJIII[Lcom/android/server/uwb/data/UwbTwoWayMeasurement;[B)V", @@ -536,7 +539,7 @@ impl<'a> TryFrom<SessionRangeDataWithEnv<'a>> for UwbRangingDataJni<'a> { JValue::Int(mac_address_indicator as i32), JValue::Int(measurements_size as i32), JValue::Object(measurements_jni.jni_context.obj), - JValue::Object(JObject::from(raw_notification_jbytearray)), + JValue::Object(raw_notification_jobject), ], )?; @@ -654,11 +657,14 @@ impl<'a> TryFrom<RangingMeasurementsWithEnv<'a>> for UwbTwoWayMeasurementJni<'a> _ => todo!(), }; let address_jbytearray = measurements_obj.env.new_byte_array(byte_arr_size)?; + + // Safety: address_jbytearray safely instantiated above. + let address_jobject = unsafe { JObject::from_raw(address_jbytearray) }; let zero_initiated_measurement_jobject = measurements_obj.env.new_object( measurements_obj.uwb_two_way_measurement_jclass, "([BIIIIIIIIIIIII)V", &[ - JValue::Object(JObject::from(address_jbytearray)), + JValue::Object(address_jobject), JValue::Int(0), JValue::Int(0), JValue::Int(0), @@ -688,11 +694,16 @@ impl<'a> TryFrom<RangingMeasurementsWithEnv<'a>> for UwbTwoWayMeasurementJni<'a> 0, mac_address_bytes.as_slice(), )?; + + // Safety: mac_address_bytes_jbytearray safely instantiated above. + let mac_address_bytes_jobject = + unsafe { JObject::from_raw(mac_address_bytes_jbytearray) }; + let measurement_jobject = measurements_obj.env.new_object( measurements_obj.uwb_two_way_measurement_jclass, "([BIIIIIIIIIIIII)V", &[ - JValue::Object(JObject::from(mac_address_bytes_jbytearray)), + JValue::Object(mac_address_bytes_jobject), JValue::Int(measurement.status as i32), JValue::Int(measurement.nlos as i32), JValue::Int(measurement.distance as i32), @@ -715,8 +726,10 @@ impl<'a> TryFrom<RangingMeasurementsWithEnv<'a>> for UwbTwoWayMeasurementJni<'a> )?; } + // Safety: measurements_array_jobject safely instantiated above. + let measurements_jobject = unsafe { JObject::from_raw(measurements_array_jobject) }; Ok(UwbTwoWayMeasurementJni { - jni_context: JniContext::new(measurements_obj.env, measurements_array_jobject.into()), + jni_context: JniContext::new(measurements_obj.env, measurements_jobject), }) } } diff --git a/service/java/com/android/server/uwb/UwbConfigStore.java b/service/java/com/android/server/uwb/UwbConfigStore.java index 4082621b..a0df3f6b 100644 --- a/service/java/com/android/server/uwb/UwbConfigStore.java +++ b/service/java/com/android/server/uwb/UwbConfigStore.java @@ -758,7 +758,7 @@ public class UwbConfigStore { * Dump the local log buffer and other internal state of UwbConfigManager. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("Dump of UwbConfigStore"); + pw.println("---- Dump of UwbConfigStore ----"); pw.println("UwbConfigStore - Store File Begin ----"); Stream.of(mSharedStores, mUserStores) .filter(Objects::nonNull) @@ -777,7 +777,7 @@ public class UwbConfigStore { pw.print(", "); pw.println("File Name: " + STORE_ID_TO_FILE_NAME.get(storeData.getStoreFileId())); } - pw.println("UwbConfigStore - Store Data End ----"); + pw.println("---- Dump of UwbConfigStore ----"); } /** diff --git a/service/java/com/android/server/uwb/UwbCountryCode.java b/service/java/com/android/server/uwb/UwbCountryCode.java index 7fbb0621..5c7c0848 100644 --- a/service/java/com/android/server/uwb/UwbCountryCode.java +++ b/service/java/com/android/server/uwb/UwbCountryCode.java @@ -331,6 +331,7 @@ public class UwbCountryCode { * Method to dump the current state of this UwbCountryCode object. */ public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("---- Dump of UwbCountryCode ----"); pw.println("DefaultCountryCode(system property): " + mUwbInjector.getOemDefaultCountryCode()); pw.println("mOverrideCountryCode: " + mOverrideCountryCode); @@ -339,5 +340,6 @@ public class UwbCountryCode { pw.println("mWifiCountryTimestamp: " + mWifiCountryTimestamp); pw.println("mCountryCode: " + mCountryCode); pw.println("mCountryCodeUpdatedTimestamp: " + mCountryCodeUpdatedTimestamp); + pw.println("---- Dump of UwbCountryCode ----"); } } diff --git a/service/java/com/android/server/uwb/UwbInjector.java b/service/java/com/android/server/uwb/UwbInjector.java index d9e8e886..25c5dd00 100644 --- a/service/java/com/android/server/uwb/UwbInjector.java +++ b/service/java/com/android/server/uwb/UwbInjector.java @@ -92,6 +92,8 @@ public class UwbInjector { private final SystemBuildProperties mSystemBuildProperties; private final UwbDiagnostics mUwbDiagnostics; + private final UwbSessionManager mUwbSessionManager; + public UwbInjector(@NonNull UwbContext context) { // Create UWB service thread. HandlerThread uwbHandlerThread = new HandlerThread("UwbService"); @@ -121,14 +123,14 @@ public class UwbInjector { UwbSessionNotificationManager uwbSessionNotificationManager = new UwbSessionNotificationManager(this); UwbAdvertiseManager uwbAdvertiseManager = new UwbAdvertiseManager(this); - UwbSessionManager uwbSessionManager = + mUwbSessionManager = new UwbSessionManager(uwbConfigurationManager, mNativeUwbManager, mUwbMetrics, uwbAdvertiseManager, uwbSessionNotificationManager, this, mContext.getSystemService(AlarmManager.class), mContext.getSystemService(ActivityManager.class), mLooper); mUwbService = new UwbServiceCore(mContext, mNativeUwbManager, mUwbMetrics, - mUwbCountryCode, uwbSessionManager, uwbConfigurationManager, this, mLooper); + mUwbCountryCode, mUwbSessionManager, uwbConfigurationManager, this, mLooper); mSystemBuildProperties = new SystemBuildProperties(); mUwbDiagnostics = new UwbDiagnostics(mContext, this, mSystemBuildProperties); } @@ -192,6 +194,10 @@ public class UwbInjector { return mUwbDiagnostics; } + public UwbSessionManager getUwbSessionManager() { + return mUwbSessionManager; + } + /** * Create a UwbShellCommand instance. */ diff --git a/service/java/com/android/server/uwb/UwbMetrics.java b/service/java/com/android/server/uwb/UwbMetrics.java index 4c2471dc..b12e6de8 100644 --- a/service/java/com/android/server/uwb/UwbMetrics.java +++ b/service/java/com/android/server/uwb/UwbMetrics.java @@ -533,24 +533,25 @@ public class UwbMetrics { public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { synchronized (mLock) { pw.println("---- Dump of UwbMetrics ----"); - pw.println("---- mRangingSessionList ----"); + pw.println("-- mRangingSessionList --"); for (RangingSessionStats stats: mRangingSessionList) { pw.println(stats.toString()); } - pw.println("---- mOpenedSessionMap ----"); + pw.println("-- mOpenedSessionMap --"); for (int i = 0; i < mOpenedSessionMap.size(); i++) { pw.println(mOpenedSessionMap.valueAt(i).toString()); } - pw.println("---- mRangingReportList ----"); + pw.println("-- mRangingReportList --"); for (RangingReportEvent event: mRangingReportList) { pw.println(event.toString()); } pw.println("mNumApps=" + mNumApps); - pw.println("---- Device operation success/error count ----"); + pw.println("-- Device operation success/error count --"); pw.println("mNumDeviceInitSuccess = " + mNumDeviceInitSuccess); pw.println("mNumDeviceInitFailure = " + mNumDeviceInitFailure); pw.println("mNumDeviceStatusError = " + mNumDeviceStatusError); pw.println("mNumUciGenericError = " + mNumUciGenericError); + pw.println("---- Dump of UwbMetrics ----"); } } } diff --git a/service/java/com/android/server/uwb/UwbServiceCore.java b/service/java/com/android/server/uwb/UwbServiceCore.java index b62aa701..853f5356 100644 --- a/service/java/com/android/server/uwb/UwbServiceCore.java +++ b/service/java/com/android/server/uwb/UwbServiceCore.java @@ -25,13 +25,12 @@ import static com.google.uwb.support.fira.FiraParams.MULTICAST_LIST_UPDATE_ACTIO 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.RemoteCallbackList; import android.os.RemoteException; import android.util.Log; import android.util.Pair; @@ -71,7 +70,6 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -95,7 +93,8 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, @VisibleForTesting public static final int TASK_NOTIFY_ADAPTER_STATE = 4; - private static final int WATCHDOG_MS = 10000; + @VisibleForTesting + public static final int WATCHDOG_MS = 10000; private static final int SEND_VENDOR_CMD_TIMEOUT_MS = 10000; @VisibleForTesting public static final int TASK_NOTIFY_ADAPTER_STATE_MESSAGE_DELAY_MS = 15000; @@ -105,8 +104,8 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, private final PowerManager.WakeLock mUwbWakeLock; private final Context mContext; - // TODO: Use RemoteCallbackList instead. - private final ConcurrentHashMap<Integer, AdapterInfo> mAdapterMap = new ConcurrentHashMap<>(); + private final RemoteCallbackList<IUwbAdapterStateCallbacks> + mAdapterStateCallbacksList = new RemoteCallbackList<>(); private final EnableDisableTask mEnableDisableTask; private final UwbSessionManager mSessionManager; @@ -269,13 +268,19 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, // TODO(b/244443764): Consider checking on the current adapter state and returning if it's // the same, to avoid sending extra onAdapterStateChanged() notifications. Currently this // will happen when UWB is toggled on and a valid country code is already set. - for (AdapterInfo adapter : mAdapterMap.values()) { + if (mAdapterStateCallbacksList.getRegisteredCallbackCount() == 0) { + return; + } + final int count = mAdapterStateCallbacksList.beginBroadcast(); + for (int i = 0; i < count; i++) { try { - adapter.getAdapterStateCallbacks().onAdapterStateChanged(adapterState, reason); + mAdapterStateCallbacksList.getBroadcastItem(i) + .onAdapterStateChanged(adapterState, reason); } catch (RemoteException e) { Log.e(TAG, "onAdapterStateChanged is failed"); } } + mAdapterStateCallbacksList.finishBroadcast(); } int getAdapterStateFromDeviceState(int deviceState) { @@ -326,17 +331,12 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, public void registerAdapterStateCallbacks(IUwbAdapterStateCallbacks adapterStateCallbacks) throws RemoteException { - AdapterInfo adapter = new AdapterInfo(Binder.getCallingPid(), adapterStateCallbacks); - mAdapterMap.put(Binder.getCallingPid(), adapter); - adapter.getBinder().linkToDeath(adapter, 0); + mAdapterStateCallbacksList.register(adapterStateCallbacks); adapterStateCallbacks.onAdapterStateChanged(getAdapterState(), mLastStateChangedReason); } public void unregisterAdapterStateCallbacks(IUwbAdapterStateCallbacks callbacks) { - int pid = Binder.getCallingPid(); - AdapterInfo adapter = mAdapterMap.get(pid); - adapter.getBinder().unlinkToDeath(adapter, 0); - mAdapterMap.remove(pid); + mAdapterStateCallbacksList.unregister(callbacks); } public void registerVendorExtensionCallback(IUwbVendorUciCallback callbacks) { @@ -412,6 +412,7 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, throw new IllegalStateException("Uwb is not enabled"); } int sessionId = 0; + int sessionType = 0; if (UuidBundleWrapper.isUuidBundle(params)) { UuidBundleWrapper uuidBundleWrapper = UuidBundleWrapper.fromBundle(params); @@ -436,14 +437,16 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, } FiraOpenSessionParams firaOpenSessionParams = builder.build(); sessionId = firaOpenSessionParams.getSessionId(); + sessionType = firaOpenSessionParams.getSessionType(); mSessionManager.initSession(attributionSource, sessionHandle, sessionId, - firaOpenSessionParams.getProtocolName(), + (byte) sessionType, firaOpenSessionParams.getProtocolName(), firaOpenSessionParams, rangingCallbacks, chipId); } else if (CccParams.isCorrectProtocol(params)) { CccOpenRangingParams cccOpenRangingParams = CccOpenRangingParams.fromBundle(params); sessionId = cccOpenRangingParams.getSessionId(); + sessionType = cccOpenRangingParams.getSessionType(); mSessionManager.initSession(attributionSource, sessionHandle, sessionId, - cccOpenRangingParams.getProtocolName(), + (byte) sessionType, cccOpenRangingParams.getProtocolName(), cccOpenRangingParams, rangingCallbacks, chipId); } else { Log.e(TAG, "openRanging - Wrong parameters"); @@ -752,7 +755,9 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, countryCode); } } finally { - mUwbWakeLock.release(); + if (mUwbWakeLock.isHeld()) { + mUwbWakeLock.release(); + } watchDog.cancel(); } } catch (Exception e) { @@ -786,7 +791,9 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, getAdapterStateFromDeviceState(UwbUciConstants.DEVICE_STATE_OFF), getReasonFromDeviceState(UwbUciConstants.DEVICE_STATE_OFF)); } finally { - mUwbWakeLock.release(); + if (mUwbWakeLock.isHeld()) { + mUwbWakeLock.release(); + } watchDog.cancel(); } } @@ -856,32 +863,6 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, } } - 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); @@ -889,14 +870,15 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, } /** - * Dump the UWB service status + * Dump the UWB session manager debug info */ public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("---- Dump of UwbServiceCore ----"); for (String chipId : mUwbInjector.getMultichipData().getChipIds()) { - pw.println("device state = " + getDeviceStateString(mChipIdToStateMap.get(chipId)) + pw.println("Device state = " + getDeviceStateString(mChipIdToStateMap.get(chipId)) + " for chip id = " + chipId); } pw.println("mLastStateChangedReason = " + mLastStateChangedReason); + pw.println("---- Dump of UwbServiceCore ----"); } } diff --git a/service/java/com/android/server/uwb/UwbServiceImpl.java b/service/java/com/android/server/uwb/UwbServiceImpl.java index 96d15ec8..b9f1990d 100644 --- a/service/java/com/android/server/uwb/UwbServiceImpl.java +++ b/service/java/com/android/server/uwb/UwbServiceImpl.java @@ -103,15 +103,22 @@ public class UwbServiceImpl extends IUwbAdapter.Stub { return; } mUwbSettingsStore.dump(fd, pw, args); + pw.println(); mUwbInjector.getUwbMetrics().dump(fd, pw, args); + pw.println(); mUwbServiceCore.dump(fd, pw, args); + pw.println(); + mUwbInjector.getUwbSessionManager().dump(fd, pw, args); + pw.println(); mUwbInjector.getUwbCountryCode().dump(fd, pw, args); + pw.println(); mUwbInjector.getUwbConfigStore().dump(fd, pw, args); + pw.println(); dumpPowerStats(fd, pw, args); } private void dumpPowerStats(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("---- powerStats ----"); + pw.println("---- PowerStats ----"); try { PersistableBundle bundle = getSpecificationInfo(null); GenericSpecificationParams params = GenericSpecificationParams.fromBundle(bundle); @@ -128,6 +135,7 @@ public class UwbServiceImpl extends IUwbAdapter.Stub { pw.println("Exception while getting power stats."); e.printStackTrace(pw); } + pw.println("---- PowerStats ----"); } private void enforceUwbPrivilegedPermission() { diff --git a/service/java/com/android/server/uwb/UwbSessionManager.java b/service/java/com/android/server/uwb/UwbSessionManager.java index 4c097ee6..bfb7096a 100644 --- a/service/java/com/android/server/uwb/UwbSessionManager.java +++ b/service/java/com/android/server/uwb/UwbSessionManager.java @@ -68,6 +68,7 @@ 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.android.server.uwb.util.LruList; import com.android.server.uwb.util.UwbUtil; import com.google.uwb.support.base.Params; @@ -84,14 +85,18 @@ import com.google.uwb.support.generic.GenericSpecificationParams; import com.google.uwb.support.oemextension.AdvertisePointedTarget; import com.google.uwb.support.oemextension.SessionStatus; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -127,8 +132,8 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification // TODO: don't expose the internal field for testing. @VisibleForTesting final ConcurrentHashMap<Integer, UwbSession> mSessionTable = new ConcurrentHashMap(); - final ConcurrentHashMap<Long, ReceivedDataInfo> mReceivedDataMap = - new ConcurrentHashMap<Long, ReceivedDataInfo>(); + // Used for storing recently closed sessions for debugging purposes. + final LruList<UwbSession> mDbgRecentlyClosedSessions = new LruList<>(5); final ConcurrentHashMap<Integer, List<UwbSession>> mNonPrivilegedUidToFiraSessionsTable = new ConcurrentHashMap(); private final ActivityManager mActivityManager; @@ -260,6 +265,12 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification Log.d(TAG, "onDataReceived - address: " + UwbUtil.toHexString(address) + ", Data: " + UwbUtil.toHexString(data)); + UwbSession uwbSession = getUwbSession((int) sessionId); + if (uwbSession == null) { + Log.e(TAG, "onDataReceived(): Received data for unknown sessionId = " + sessionId); + return; + } + // Size of address in the UCI Packet for DATA_MESSAGE_RCV is always expected to be 8 // (EXTENDED_ADDRESS_BYTE_LENGTH). It can contain the MacAddress in short format however // (2 LSB with MacAddress, 6 MSB zeroed out). @@ -278,10 +289,12 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification info.sourceEndPoint = sourceEndPoint; info.destEndPoint = destEndPoint; info.payload = data; - mReceivedDataMap.put(longAddress, info); + + uwbSession.addReceivedDataInfo(info); } - private static final class ReceivedDataInfo { + @VisibleForTesting + static final class ReceivedDataInfo { public long sessionId; public int status; public long sequenceNum; @@ -370,16 +383,6 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification } } - 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) { int status = mConfigurationManager.setAppConfigurations(uwbSession.getSessionId(), uwbSession.getParams(), uwbSession.getChipId()); @@ -396,13 +399,13 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification } public synchronized void initSession(AttributionSource attributionSource, - SessionHandle sessionHandle, int sessionId, String protocolName, Params params, - IUwbRangingCallbacks rangingCallbacks, String chipId) + SessionHandle sessionHandle, int sessionId, byte sessionType, String protocolName, + Params params, IUwbRangingCallbacks rangingCallbacks, String chipId) throws RemoteException { - Log.i(TAG, "initSession() - sessionId: " + sessionId - + ", sessionHandle: " + sessionHandle); + Log.i(TAG, "initSession() - sessionId: " + sessionId + ", sessionHandle: " + sessionHandle + + ", sessionType: " + sessionType); UwbSession uwbSession = createUwbSession(attributionSource, sessionHandle, sessionId, - protocolName, params, rangingCallbacks, chipId); + sessionType, protocolName, params, rangingCallbacks, chipId); // Check the attribution source chain to ensure that there are no 3p apps which are not in // fg which can receive the ranging results. AttributionSource nonPrivilegedAppAttrSource = @@ -445,8 +448,6 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification return; } - byte sessionType = getSessionType(protocolName); - try { uwbSession.getBinder().linkToDeath(uwbSession, 0); } catch (RemoteException e) { @@ -470,10 +471,10 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification // TODO: use UwbInjector. @VisibleForTesting UwbSession createUwbSession(AttributionSource attributionSource, SessionHandle sessionHandle, - int sessionId, String protocolName, Params params, + int sessionId, byte sessionType, String protocolName, Params params, IUwbRangingCallbacks iUwbRangingCallbacks, String chipId) { - return new UwbSession(attributionSource, sessionHandle, sessionId, protocolName, params, - iUwbRangingCallbacks, chipId); + return new UwbSession(attributionSource, sessionHandle, sessionId, sessionType, + protocolName, params, iUwbRangingCallbacks, chipId); } public synchronized void deInitSession(SessionHandle sessionHandle) { @@ -595,30 +596,18 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification UwbOwrAoaMeasurement uwbOwrAoaMeasurement = rangingData.getRangingOwrAoaMeasure(); mAdvertiseManager.updateAdvertiseTarget(uwbOwrAoaMeasurement); - byte[] macAddress = getValidMacAddressFromOwrAoaMeasurement( + byte[] macAddressBytes = getValidMacAddressFromOwrAoaMeasurement( rangingData, uwbOwrAoaMeasurement); - if (macAddress == null) { + if (macAddressBytes == null) { Log.i(TAG, "OwR Aoa UwbSession: Invalid MacAddress for remote device"); return; } - uwbSession.setRemoteMacAddress(macAddress); - // Get any application payload data received in this OWR AOA ranging session and notify it. - ReceivedDataInfo receivedDataInfo = getReceivedDataInfo(macAddress); - if (receivedDataInfo == null) { - return; - } - - UwbSession uwbSessionFromReceivedData = getUwbSession((int) receivedDataInfo.sessionId); - if (uwbSessionFromReceivedData != uwbSession) { - return; - } - - boolean advertisePointingResult = mAdvertiseManager.isPointedTarget(macAddress); + boolean advertisePointingResult = mAdvertiseManager.isPointedTarget(macAddressBytes); if (mUwbInjector.getUwbServiceCore().isOemExtensionCbRegistered()) { try { PersistableBundle pointedTargetBundle = new AdvertisePointedTarget.Builder() - .setMacAddress(macAddress) + .setMacAddress(macAddressBytes) .setAdvertisePointingResult(advertisePointingResult) .build() .toBundle(); @@ -633,9 +622,22 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification } if (advertisePointingResult) { - UwbAddress uwbAddress = UwbAddress.fromBytes(macAddress); - mSessionNotificationManager.onDataReceived( - uwbSession, uwbAddress, new PersistableBundle(), receivedDataInfo.payload); + // Use a loop to notify all the received application data payload(s) (in sequence number + // order) for this OWR AOA ranging session. + long macAddress = macAddressByteArrayToLong(macAddressBytes); + UwbAddress uwbAddress = UwbAddress.fromBytes(macAddressBytes); + + List<ReceivedDataInfo> receivedDataInfoList = uwbSession.getAllReceivedDataInfo( + macAddress); + if (receivedDataInfoList.isEmpty()) { + Log.i(TAG, "OwR Aoa UwbSession: Application Payload data not found for" + + " MacAddress = " + UwbUtil.toHexString(macAddress)); + return; + } + + receivedDataInfoList.stream().forEach(r -> + mSessionNotificationManager.onDataReceived( + uwbSession, uwbAddress, new PersistableBundle(), r.payload)); mAdvertiseManager.removeAdvertiseTarget(macAddress); } } @@ -652,13 +654,6 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification return null; } - /** Get any received data for the given device MacAddress */ - @VisibleForTesting - public ReceivedDataInfo getReceivedDataInfo(byte[] macAddress) { - // Convert the macAddress to a long as the address could be in short or extended format. - return mReceivedDataMap.get(macAddressByteArrayToLong(macAddress)); - } - public boolean isExistedSession(SessionHandle sessionHandle) { return (getSessionId(sessionHandle) != null); } @@ -861,18 +856,18 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification removeFromNonPrivilegedUidToFiraSessionTableIfNecessary(uwbSession); removeAdvertiserData(uwbSession); mSessionTable.remove(uwbSession.getSessionId()); + mDbgRecentlyClosedSessions.add(uwbSession); } } private void removeAdvertiserData(UwbSession uwbSession) { - byte[] remoteMacAddress = uwbSession.getRemoteMacAddress(); - if (remoteMacAddress != null) { + for (long remoteMacAddress : uwbSession.getRemoteMacAddressList()) { mAdvertiseManager.removeAdvertiseTarget(remoteMacAddress); } } void addToNonPrivilegedUidToFiraSessionTableIfNecessary(@NonNull UwbSession uwbSession) { - if (getSessionType(uwbSession.getProtocolName()) == UwbUciConstants.SESSION_TYPE_RANGING) { + if (uwbSession.getSessionType() == UwbUciConstants.SESSION_TYPE_RANGING) { AttributionSource nonPrivilegedAppAttrSource = uwbSession.getAnyNonPrivilegedAppInAttributionSource(); if (nonPrivilegedAppAttrSource != null) { @@ -886,7 +881,7 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification } void removeFromNonPrivilegedUidToFiraSessionTableIfNecessary(@NonNull UwbSession uwbSession) { - if (getSessionType(uwbSession.getProtocolName()) == UwbUciConstants.SESSION_TYPE_RANGING) { + if (uwbSession.getSessionType() == UwbUciConstants.SESSION_TYPE_RANGING) { AttributionSource nonPrivilegedAppAttrSource = uwbSession.getAnyNonPrivilegedAppInAttributionSource(); if (nonPrivilegedAppAttrSource != null) { @@ -1015,7 +1010,7 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification uwbSession.setOperationType(OPERATION_TYPE_INIT_SESSION); status = mNativeUwbManager.initSession( uwbSession.getSessionId(), - getSessionType(uwbSession.getParams().getProtocolName()), + uwbSession.getSessionType(), uwbSession.getChipId()); if (status != UwbUciConstants.STATUS_CODE_OK) { return status; @@ -1425,20 +1420,21 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification return sendDataStatus; } - // TODO(b/246678053): Check on the usage of sequenceNum field, is it used - // for ordering the data payload packets by host or firmware ? - int sequenceNum = 1; + // Get the UCI sequence number for this data packet. + byte sequenceNum = uwbSession.getDataSndSequenceNumber(); sendDataStatus = mNativeUwbManager.sendData( uwbSession.getSessionId(), sendDataInfo.remoteDeviceAddress.toBytes(), UwbUciConstants.UWB_DESTINATION_END_POINT_HOST, sequenceNum, - sendDataInfo.data); - Log.d(TAG, "MSG_SESSION_SEND_DATA status: " + sendDataStatus); + sendDataInfo.data, uwbSession.getChipId()); + Log.d(TAG, "MSG_SESSION_SEND_DATA status: " + sendDataStatus + + " for data packet sequence number: " + sequenceNum); if (sendDataStatus == UwbUciConstants.STATUS_CODE_OK) { mSessionNotificationManager.onDataSent( uwbSession, sendDataInfo.remoteDeviceAddress, sendDataInfo.params); + uwbSession.incrementDataSndSequenceNumber(); } else { mSessionNotificationManager.onDataSendFailed( uwbSession, sendDataInfo.remoteDeviceAddress, sendDataStatus, @@ -1516,7 +1512,7 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification private final AttributionSource mAttributionSource; private final SessionHandle mSessionHandle; private final int mSessionId; - private byte[] mRemoteMacAddress; + private final byte mSessionType; private final IUwbRangingCallbacks mIUwbRangingCallbacks; private final String mProtocolName; private final IBinder mIBinder; @@ -1532,16 +1528,29 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification private boolean mHasNonPrivilegedFgApp = false; private @FiraParams.RangeDataNtfConfig Integer mOrigRangeDataNtfConfig; private long mRangingErrorStreakTimeoutMs = RANGING_RESULT_ERROR_NO_TIMEOUT; + // Use a Map<RemoteMacAddress, SortedMap<SequenceNumber, ReceivedDataInfo>> to store all + // the Application payload data packets received in this (active) UWB Session. + // - The outer key (RemoteMacAddress) is used to identify the Advertiser device that sends + // the data (there can be multiple advertisers in the same UWB session). + // - The inner key (SequenceNumber) is used to ensure we don't store duplicate packets, + // and notify them to the higher layers in-order. + // TODO(b/246678053): Change the type of SequenceNumber from Long to Integer everywhere. + private final ConcurrentHashMap<Long, SortedMap<Long, ReceivedDataInfo>> + mReceivedDataInfoMap; + + // Store the UCI sequence number for the next Data packet (to be sent to UWBS). + private byte mDataSndSequenceNumber; @VisibleForTesting public List<UwbControlee> mControleeList; UwbSession(AttributionSource attributionSource, SessionHandle sessionHandle, int sessionId, - String protocolName, Params params, IUwbRangingCallbacks iUwbRangingCallbacks, - String chipId) { + byte sessionType, String protocolName, Params params, + IUwbRangingCallbacks iUwbRangingCallbacks, String chipId) { this.mAttributionSource = attributionSource; this.mSessionHandle = sessionHandle; this.mSessionId = sessionId; + this.mSessionType = sessionType; this.mProtocolName = protocolName; this.mIUwbRangingCallbacks = iUwbRangingCallbacks; this.mIBinder = iUwbRangingCallbacks.asBinder(); @@ -1562,6 +1571,9 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification mRangingErrorStreakTimeoutMs = firaParams .getRangingErrorStreakTimeoutMs(); } + + this.mReceivedDataInfoMap = new ConcurrentHashMap<>(); + this.mDataSndSequenceNumber = 0; } private boolean isPrivilegedApp(int uid, String packageName) { @@ -1594,6 +1606,51 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification } /** + * Store a ReceivedDataInfo for the UwbSession. If we already have stored data from the + * same advertiser and with the same sequence number, this is a no-op. + */ + public void addReceivedDataInfo(ReceivedDataInfo receivedDataInfo) { + SortedMap<Long, ReceivedDataInfo> innerMap = mReceivedDataInfoMap.get( + receivedDataInfo.address); + if (innerMap == null) { + innerMap = new TreeMap<>(); + mReceivedDataInfoMap.put(receivedDataInfo.address, innerMap); + } + innerMap.putIfAbsent(receivedDataInfo.sequenceNum, receivedDataInfo); + } + + /** + * Return all the ReceivedDataInfo from the given remote device, in sequence number order. + * This method also removes the returned packets from the Map, so the same packet will + * not be returned again (in a future call). + */ + public List<ReceivedDataInfo> getAllReceivedDataInfo(long macAddress) { + SortedMap<Long, ReceivedDataInfo> innerMap = mReceivedDataInfoMap.get(macAddress); + if (innerMap == null) { + // No stored ReceivedDataInfo(s) for the address. + return List.of(); + } + + List<ReceivedDataInfo> receivedDataInfoList = new ArrayList<>(innerMap.values()); + innerMap.clear(); + return receivedDataInfoList; + } + + /** + * Get the UCI sequence number for the next Data packet to be sent to the UWBS. + */ + public byte getDataSndSequenceNumber() { + return mDataSndSequenceNumber; + } + + /** + * Increment the UCI sequence number for the next Data packet to be sent to the UWBS. + */ + public void incrementDataSndSequenceNumber() { + mDataSndSequenceNumber++; + } + + /** * Adds a Controlee to the session. This should only be called to reflect * the state of the native UWB interface. * @param address The UWB address of the Controlee to add. @@ -1624,6 +1681,10 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification return this.mSessionId; } + public byte getSessionType() { + return this.mSessionType; + } + public String getChipId() { return this.mChipId; } @@ -1705,12 +1766,8 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification this.mSessionState = state; } - public byte[] getRemoteMacAddress() { - return mRemoteMacAddress; - } - - public void setRemoteMacAddress(byte[] remoteMacAddress) { - this.mRemoteMacAddress = Arrays.copyOf(remoteMacAddress, remoteMacAddress.length); + public Set<Long> getRemoteMacAddressList() { + return mReceivedDataInfoMap.keySet(); } public void setMulticastListUpdateStatus( @@ -1780,7 +1837,6 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification } } - /** * Starts a timer to detect if the app that started the UWB session is in the background * for longer than {@link UwbSession#mNonPrivilegedBgTimeoutMs }. @@ -1866,6 +1922,18 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification } } } + + @Override + public String toString() { + return "UwbSession: { Session Id: " + getSessionId() + + ", Handle: " + getSessionHandle() + + ", Protocol: " + getProtocolName() + + ", State: " + getSessionState() + + ", Data Send Sequence Number: " + getDataSndSequenceNumber() + + ", Params: " + getParams() + + ", AttributionSource: " + getAttributionSource() + + " }"; + } } // TODO: refactor the async operation flow. @@ -1883,4 +1951,31 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification notify(); } } -}
\ No newline at end of file + + /** + * Dump the UWB session manager debug info + */ + public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("---- Dump of UwbSessionManager ----"); + pw.println("Active sessions: "); + for (Map.Entry<Integer, UwbSession> entry : mSessionTable.entrySet()) { + UwbSession uwbSession = entry.getValue(); + pw.println(uwbSession); + } + pw.println("Recently closed sessions: "); + for (UwbSession uwbSession: mDbgRecentlyClosedSessions.getEntries()) { + pw.println(uwbSession); + } + List<Integer> nonPrivilegedSessionIds = + mNonPrivilegedUidToFiraSessionsTable.entrySet() + .stream() + .map(e -> e.getValue() + .stream() + .map(UwbSession::getSessionId) + .collect(Collectors.toList())) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + pw.println("Non Privileged Fira Session Ids: " + nonPrivilegedSessionIds); + pw.println("---- Dump of UwbSessionManager ----"); + } +} diff --git a/service/java/com/android/server/uwb/UwbSessionNotificationManager.java b/service/java/com/android/server/uwb/UwbSessionNotificationManager.java index 0c095dd8..4e278e86 100644 --- a/service/java/com/android/server/uwb/UwbSessionNotificationManager.java +++ b/service/java/com/android/server/uwb/UwbSessionNotificationManager.java @@ -68,15 +68,22 @@ public class UwbSessionNotificationManager { + sessionHandle); return; } - - RangingReport rangingReport = getRangingReport(rangingData, uwbSession.getProtocolName(), - uwbSession.getParams(), mUwbInjector.getElapsedSinceBootNanos()); + RangingReport rangingReport = null; + try { + rangingReport = getRangingReport(rangingData, + uwbSession.getProtocolName(), + uwbSession.getParams(), mUwbInjector.getElapsedSinceBootNanos()); + } catch (Exception e) { + Log.e(TAG, "getRangingReport Failed."); + e.printStackTrace(); + } if (mUwbInjector.getUwbServiceCore().isOemExtensionCbRegistered()) { try { rangingReport = mUwbInjector.getUwbServiceCore().getOemExtensionCallback() .onRangingReportReceived(rangingReport); } catch (RemoteException e) { + Log.e(TAG, "UwbInjector - onRangingReportReceived : Failed."); e.printStackTrace(); } } diff --git a/service/java/com/android/server/uwb/UwbSettingsStore.java b/service/java/com/android/server/uwb/UwbSettingsStore.java index 24e59727..3abf5d8b 100644 --- a/service/java/com/android/server/uwb/UwbSettingsStore.java +++ b/service/java/com/android/server/uwb/UwbSettingsStore.java @@ -282,11 +282,11 @@ public class UwbSettingsStore { * Dump output for debugging. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println(); - pw.println("Dump of " + TAG); + pw.println("---- Dump of UwbSettingsStore ----"); synchronized (mLock) { pw.println("Settings: " + mSettings); } + pw.println("---- Dump of UwbSettingsStore ----"); } /** diff --git a/service/java/com/android/server/uwb/UwbShellCommand.java b/service/java/com/android/server/uwb/UwbShellCommand.java index af5b0c3d..d147d19f 100644 --- a/service/java/com/android/server/uwb/UwbShellCommand.java +++ b/service/java/com/android/server/uwb/UwbShellCommand.java @@ -142,6 +142,7 @@ public class UwbShellCommand extends BasicShellCommandHandler { new FiraOpenSessionParams.Builder() .setProtocolVersion(FiraParams.PROTOCOL_VERSION_1_1) .setSessionId(1) + .setSessionType(FiraParams.SESSION_TYPE_RANGING) .setChannelNumber(9) .setDeviceType(RANGING_DEVICE_TYPE_CONTROLLER) .setDeviceRole(RANGING_DEVICE_ROLE_INITIATOR) diff --git a/service/java/com/android/server/uwb/UwbTestUtils.java b/service/java/com/android/server/uwb/UwbTestUtils.java index c5358841..d0ae15fe 100644 --- a/service/java/com/android/server/uwb/UwbTestUtils.java +++ b/service/java/com/android/server/uwb/UwbTestUtils.java @@ -45,16 +45,23 @@ import com.google.uwb.support.oemextension.RangingReportMetadata; public class UwbTestUtils { public static final int TEST_SESSION_ID = 7; public static final int TEST_SESSION_ID_2 = 8; + public static final byte TEST_SESSION_TYPE = FiraParams.SESSION_TYPE_RANGING; public static final byte[] PEER_SHORT_MAC_ADDRESS = {0x35, 0x37}; + public static final long PEER_SHORT_MAC_ADDRESS_LONG = 0x3735L; public static final byte[] PEER_EXTENDED_SHORT_MAC_ADDRESS = {0x35, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + public static final long PEER_EXTENDED_SHORT_MAC_ADDRESS_LONG = 0x3735L; public static final byte[] PEER_EXTENDED_MAC_ADDRESS = {0x12, 0x14, 0x16, 0x18, 0x31, 0x33, 0x35, 0x37}; + public static final long PEER_EXTENDED_MAC_ADDRESS_LONG = 0x3735333118161412L; public static final byte[] PEER_EXTENDED_MAC_ADDRESS_2 = {0x2, 0x4, 0x6, 0x8, 0x1, 0x3, 0x5, 0x7}; + public static final long PEER_EXTENDED_MAC_ADDRESS_2_LONG = 0x0705030108060402L; public static final byte[] PEER_BAD_MAC_ADDRESS = {0x12, 0x14, 0x16, 0x18}; public static final UwbAddress PEER_EXTENDED_UWB_ADDRESS = UwbAddress.fromBytes( PEER_EXTENDED_MAC_ADDRESS); + public static final UwbAddress PEER_EXTENDED_UWB_ADDRESS_2 = UwbAddress.fromBytes( + PEER_EXTENDED_MAC_ADDRESS_2); public static final UwbAddress PEER_SHORT_UWB_ADDRESS = UwbAddress.fromBytes( PEER_SHORT_MAC_ADDRESS); public static final PersistableBundle PERSISTABLE_BUNDLE = new PersistableBundle(); @@ -72,6 +79,7 @@ public class UwbTestUtils { 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 int TEST_BAD_AOA_AZIMUTH_FOM = 150; 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; @@ -101,11 +109,22 @@ public class UwbTestUtils { /** Build UwbRangingData for all Ranging Measurement Type(s). */ public static UwbRangingData generateRangingData( int rangingMeasurementType, int macAddressingMode, int rangingStatus) { + byte[] macAddress = (macAddressingMode == MAC_ADDRESSING_MODE_SHORT) + ? PEER_SHORT_MAC_ADDRESS : PEER_EXTENDED_MAC_ADDRESS; + return generateRangingData( + rangingMeasurementType, macAddressingMode, macAddress, rangingStatus); + } + + /** Build UwbRangingData for all Ranging Measurement Type(s). */ + public static UwbRangingData generateRangingData( + int rangingMeasurementType, int macAddressingMode, byte[] macAddress, + int rangingStatus) { switch (rangingMeasurementType) { case RANGING_MEASUREMENT_TYPE_TWO_WAY: return generateTwoWayMeasurementRangingData(rangingStatus); case RANGING_MEASUREMENT_TYPE_OWR_AOA: - return generateOwrAoaMeasurementRangingData(macAddressingMode, rangingStatus); + return generateOwrAoaMeasurementRangingData( + macAddressingMode, macAddress, rangingStatus); case RANGING_MEASUREMENT_TYPE_DL_TDOA: return generateDlTDoAMeasurementRangingData(macAddressingMode, rangingStatus); default: @@ -130,10 +149,8 @@ public class UwbTestUtils { } private static UwbRangingData generateOwrAoaMeasurementRangingData( - int macAddressingMode, int rangingStatus) { + int macAddressingMode, byte[] macAddress, int rangingStatus) { final int noOfRangingMeasures = 1; - byte[] macAddress = (macAddressingMode == MAC_ADDRESSING_MODE_SHORT) - ? PEER_SHORT_MAC_ADDRESS : PEER_EXTENDED_MAC_ADDRESS; final UwbOwrAoaMeasurement uwbOwrAoaMeasurement = new UwbOwrAoaMeasurement( macAddress, rangingStatus, TEST_LOS, TEST_FRAME_SEQUENCE_NUMBER, TEST_BLOCK_IDX, @@ -145,6 +162,21 @@ public class UwbTestUtils { TEST_RAW_NTF_DATA); } + /** Generate an OWR ranging data with a bad AoA Azimuth FOM */ + public static UwbRangingData generateBadOwrAoaMeasurementRangingData( + int macAddressingMode, byte[] macAddress) { + final int noOfRangingMeasures = 1; + final UwbOwrAoaMeasurement uwbOwrAoaMeasurement = new UwbOwrAoaMeasurement( + macAddress, TEST_STATUS, TEST_LOS, + TEST_FRAME_SEQUENCE_NUMBER, TEST_BLOCK_IDX, + convertFloatToQFormat(TEST_AOA_AZIMUTH, 9, 7), TEST_BAD_AOA_AZIMUTH_FOM, + convertFloatToQFormat(TEST_AOA_ELEVATION, 9, 7), TEST_AOA_ELEVATION_FOM); + return new UwbRangingData(TEST_SEQ_COUNTER, TEST_SESSION_ID, + TEST_RCR_INDICATION, TEST_CURR_RANGING_INTERVAL, RANGING_MEASUREMENT_TYPE_OWR_AOA, + macAddressingMode, noOfRangingMeasures, uwbOwrAoaMeasurement, + TEST_RAW_NTF_DATA); + } + private static UwbRangingData generateDlTDoAMeasurementRangingData( int macAddressingMode, int rangingStatus) { final int noOfRangingMeasures = 1; diff --git a/service/java/com/android/server/uwb/advertisement/UwbAdvertiseManager.java b/service/java/com/android/server/uwb/advertisement/UwbAdvertiseManager.java index 0355f604..6f4085dd 100644 --- a/service/java/com/android/server/uwb/advertisement/UwbAdvertiseManager.java +++ b/service/java/com/android/server/uwb/advertisement/UwbAdvertiseManager.java @@ -85,8 +85,7 @@ public class UwbAdvertiseManager { /** * Remove all the stored AdvertiseTarget data for the given device. */ - public void removeAdvertiseTarget(byte[] macAddressBytes) { - long macAddress = macAddressByteArrayToLong(macAddressBytes); + public void removeAdvertiseTarget(long macAddress) { mAdvertiseTargetMap.remove(macAddress); } @@ -113,7 +112,7 @@ public class UwbAdvertiseManager { } if (!isWithinTimeThreshold(uwbAdvertiseTarget)) { - removeAdvertiseTarget(macAddressBytes); + removeAdvertiseTarget(macAddress); } } diff --git a/service/java/com/android/server/uwb/correction/UwbFilterEngine.java b/service/java/com/android/server/uwb/correction/UwbFilterEngine.java new file mode 100644 index 00000000..4617f8a9 --- /dev/null +++ b/service/java/com/android/server/uwb/correction/UwbFilterEngine.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.server.uwb.correction.filtering.IPositionFilter; +import com.android.server.uwb.correction.math.Pose; +import com.android.server.uwb.correction.math.SphericalVector; +import com.android.server.uwb.correction.pose.IPoseSource; +import com.android.server.uwb.correction.primers.IPrimer; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Consumes raw UWB values and outputs filtered UWB values. See the {@link UwbFilterEngine.Builder} + * for how it is configured. + */ +public class UwbFilterEngine implements AutoCloseable { + public static final boolean ENABLE_BIG_LOG = false; + @NonNull private final List<IPrimer> mPrimers; + @Nullable private final IPositionFilter mFilter; + @Nullable private final IPoseSource mPoseSource; + + /** + * The last UWB reading, after priming or filtering, depending on which facilities + * are available. If computation fails or is not possible (ie - filter or primer is not + * configured), the computation function will return this. + */ + @Nullable private SphericalVector mLastInputState; + + private boolean mClosed; + + private UwbFilterEngine( + @NonNull List<IPrimer> primers, + @Nullable IPoseSource poseSource, + @Nullable IPositionFilter filter) { + this.mPrimers = primers; + this.mPoseSource = poseSource; + this.mFilter = filter; + } + + /** + * Updates the engine with the latest UWB data. + * @param position The raw position produced by the UWB hardware. + */ + public void add(@NonNull SphericalVector.Sparse position) { + add(position, Instant.now()); + } + + /** + * Updates the engine with the latest UWB data. + * @param position The raw position produced by the UWB hardware. + * @param instant The instant at which the UWB value was received. + */ + public void add(@NonNull SphericalVector.Sparse position, Instant instant) { + StringBuilder bigLog = ENABLE_BIG_LOG ? new StringBuilder(position.toString()) : null; + Objects.requireNonNull(position); + Objects.requireNonNull(instant); + + SphericalVector prediction = compute(instant); + + for (IPrimer primer: mPrimers) { + position = primer.prime(position, prediction, mPoseSource); + if (bigLog != null) { + bigLog.append(" ->") + .append(primer.getClass().getSimpleName()).append("=") + .append(position); + } + } + if (!position.isComplete()) { + // Primers did not fully prime the position vector. + // This is not okay unless the triangulation filter is implemented to produce + // these missing values. + } + mLastInputState = position.vector; + if (mFilter != null) { + mFilter.updatePose(mPoseSource, instant); + mFilter.add(position.vector, instant); + if (bigLog != null) { + bigLog.append(" : filtered=") + .append(mFilter.compute()); + } + } + if (bigLog != null) { + Log.d("RAW", bigLog.toString()); + } + } + + /** + * Computes the most probably UWB location as of now. + * + * @return A SphericalVector representing the most likely UWB location. + */ + @Nullable + public SphericalVector compute() { + return compute(Instant.now()); + } + + /** + * Computes the most probable UWB location as of the given instant. + * @param instant The time for which to compute the UWB location. This should be at or after + * the most recent UWB sample. + * @return A SphericalVector representing the most likely UWB location. + */ + @Nullable + public SphericalVector compute(Instant instant) { + if (mFilter != null) { + mFilter.updatePose(mPoseSource, instant); + return mFilter.compute(instant); + } + return mLastInputState; + } + + /** + * Gets the current device pose. + */ + @NonNull + public Pose getPose() { + Pose pose = null; + if (mPoseSource != null) { + pose = mPoseSource.getPose(); + } + if (pose == null) { + pose = Pose.IDENTITY; + } + return pose; + } + + /** + * Frees or closes all resources consumed by this object. + */ + @Override + public void close() { + if (!mClosed) { + mClosed = true; + } + } + + /** + * Builder for a {@link UwbFilterEngine}. + */ + public static class Builder { + @Nullable private IPositionFilter mFilter; + @Nullable private IPoseSource mPoseSource; + @NonNull private final ArrayList<IPrimer> mPrimers = new ArrayList<>(); + + /** + * Sets the filter this UWB filter engine will use. If not provided, no filtering will + * occur. + * @param filter The position filter to use. + * @return This builder. + */ + public Builder setFilter(IPositionFilter filter) { + this.mFilter = filter; + return this; + } + + /** + * Sets the pose source the UWB filter engine will use. If not set, no pose processing + * will occur. + * @param poseSource Any pose source. + * @return This builder. + */ + public Builder setPoseSource(IPoseSource poseSource) { + this.mPoseSource = poseSource; + return this; + } + + /** + * Adds a primer to the list of primers the engine will use. The primers will execute + * in the order in which {@link #addPrimer(IPrimer)} was called. + * @param primer The primer to add. + * @return This builder. + */ + public Builder addPrimer(@NonNull IPrimer primer) { + Objects.requireNonNull(primer); + this.mPrimers.add(primer); + return this; + } + + /** + * Builds a UWB filter engine based on the calls made to the builder. + * @return the constructed UWB filter engine. + */ + public UwbFilterEngine build() { + return new UwbFilterEngine(mPrimers, mPoseSource, mFilter); + } + } +} diff --git a/service/java/com/android/server/uwb/correction/filtering/IFilter.java b/service/java/com/android/server/uwb/correction/filtering/IFilter.java new file mode 100644 index 00000000..7413a1ae --- /dev/null +++ b/service/java/com/android/server/uwb/correction/filtering/IFilter.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.filtering; + +import androidx.annotation.NonNull; + +import java.time.Instant; + +/** + * Interface for a filter. + */ +public interface IFilter { + /** + * Adds a value to the filter. + * @param value The value to add to the filter. + * The timestamp defaults to now. + */ + default void add(float value) { + add(value, Instant.now()); + } + + /** + * Adds a value to the filter. + * @param value The value to add to the filter. + * @param instant When the value occurred, used to determine the latency introduced by + * the filter. Note that this has no effect on the order in which the filter operates + * on values. + */ + void add(float value, @NonNull Instant instant); + + /** + * Alters the state of the filter such that it anticipates a change by the given amount. + * For example, if the filter is working with distance, and the distance of the next + * reading is expected to increase by 1 meter, 'shift' should be 1. + * @param shift How much to alter the filter state. + */ + void compensate(float shift); + + /** + * Gets a sample object with the result from the last computation. The sample's time is + * the average time of the samples that created the result, effectively describing the + * latency introduced by the filter. + * @return The result from the last computation. + */ + @NonNull + Sample getResult(); + + /** + * Gets a sample object with the result from the provided time. The returned sample's time is + * the closest the filter can provide to the given time. + * The default behavior is to return the latest available result, which is likely to be + * older than the requested time (see {@link #getResult()}). + * This must be overridden in order to support predicting filters like a Kalman filter or an + * extrapolating median/average filter. + * @param when The preferred time of the predicted sample. + * @return The result from the computation. + */ + @NonNull + default Sample getResult(Instant when) { + return getResult(); + } +} diff --git a/service/java/com/android/server/uwb/correction/filtering/IPositionFilter.java b/service/java/com/android/server/uwb/correction/filtering/IPositionFilter.java new file mode 100644 index 00000000..091bb8d1 --- /dev/null +++ b/service/java/com/android/server/uwb/correction/filtering/IPositionFilter.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.filtering; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.server.uwb.correction.math.SphericalVector; +import com.android.server.uwb.correction.pose.IPoseSource; + +import java.time.Instant; + +/** + * Interface for a filter that operates on a UwbPosition. + */ +public interface IPositionFilter { + /** + * Adds a value to the filter. + * @param position The value to add to the filter. + * The timestamp defaults to now. + */ + default void add(@NonNull SphericalVector position) { + add(position, Instant.now()); + } + + /** + * Adds a value to the filter. + * @param value The value to add to the filter. + * @param instant When the value occurred, used to determine the latency introduced by + * the filter. Note that this has no effect on the order in which the filter operates + * on values. + */ + void add(@NonNull SphericalVector value, @NonNull Instant instant); + + /** + * Computes a predicted UWB position based on the new pose. + */ + default SphericalVector compute() { + return compute(Instant.now()); + } + + /** + * Computes a predicted UWB position based on the new pose. + * @param instant The instant for which the UWB prediction should be computed. + */ + SphericalVector compute(@NonNull Instant instant); + + /** + * Updates the filter history to account for changes to the pose. + * @param poseSource The pose source from which to get the latest pose. + */ + void updatePose(@Nullable IPoseSource poseSource, @NonNull Instant instant); +} diff --git a/service/java/com/android/server/uwb/correction/filtering/MAFilter.java b/service/java/com/android/server/uwb/correction/filtering/MAFilter.java new file mode 100644 index 00000000..eba0b4f1 --- /dev/null +++ b/service/java/com/android/server/uwb/correction/filtering/MAFilter.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.filtering; + +import androidx.annotation.NonNull; + +import java.time.Instant; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * A Median, Average filter. The filter has an adjustable median window and + * the configured percentage of non-outliers are averaged. + */ +public class MAFilter implements IFilter { + /** + * The maximum allowed filter size. + */ + public static final int MAX_FILTER = 255; + private static final String TAG = "MAEFilter"; + + private int mWindowSize; + private float mCut; + @NonNull + private final ArrayDeque<Sample> mWindow = new ArrayDeque<>(); + @NonNull + private Sample mResult = new Sample(0F, Instant.now()); + + /** + * Creates a new instance of the MAFilter class. + * @param windowSize The maximum number of samples to store in the moving window. + * @param cut What percentage of non-outliers are to be averaged, from 0 to 1. See + * {@link #setCut(float)} for more information. + */ + public MAFilter(int windowSize, float cut) { + setWindowSize(windowSize); + setCut(cut); + } + + /** + * Gets the size of the median window. + * @return A count of samples. + */ + public int getWindowSize() { + return mWindowSize; + } + + /** + * Sets the size of the median window; how many samples are considered when producing a filtered + * result. Must be between 1 and {@link #MAX_FILTER}. + * @param value The number of samples to set as the maximum window size. + */ + public void setWindowSize(int value) { + if (value <= 0 || value > MAX_FILTER) { + throw new IllegalArgumentException( + "Value is out of range; must be between 1 and " + MAX_FILTER + " inclusive."); + } + mWindowSize = value; + } + + /** + * Gets the size of the median cut. + * @return A value from 0-1 that describes what percentage of values in the window will be + * kept and averaged. + */ + public float getCut() { + return mCut; + } + + /** + * Sets the size of the median cut. A value of 0 is a perfect median, taking only the + * center value(s). A value of 1 is a perfect average. A value of 0.25 discards 75% of the + * outliers and averages the 25% remaining values. + * @param value A value 0-1 that describes what median percentage of the window to average. + */ + public void setCut(float value) { + if (value < 0 || value > 1) { + throw new IllegalArgumentException( + "Value is out of range; must be between 0 and 1 inclusive"); + } + mCut = value; + } + + /** + * Gets a sample object with the result from the last computation. The sample's time is + * the average time of the samples that created the result, effectively describing the + * latency introduced by the filter. + * @return The result from the last computation. + */ + @NonNull + public Sample getResult() { + return mResult; + } + + /** + * Adds a value to the filter. + * @param value The value to add to the filter. + * @param instant When the value occurred, used to determine the latency introduced by + * the filter. Note that this has no effect on the order in which the filter operates + * on values. Defaults to now. + */ + @Override + public void add(float value, @NonNull Instant instant) { + Objects.requireNonNull(instant); + mWindow.addLast(new Sample(value, instant)); + while (mWindow.size() > mWindowSize) { + mWindow.removeFirst(); + } + mResult = compute(); + } + + /** + * Rewrites all sample values based on the selector. + * @param selector The interface containing the function that selects the new sample values. + */ + protected void remap(RemapFunction selector) { + mWindow.forEach(s -> s.value = selector.run(s.value)); + mResult = new Sample(selector.run(mResult.value), mResult.instant); + } + + /** + * Alters the state of the filter such that it anticipates a change by the given amount. + * For example, if the filter is working with distance, and the distance of the next + * reading is expected to increase by 1 meter, 'shift' should be 1. + * @param shift How much to alter the filter state. + */ + @Override + public void compensate(float shift) { + remap(s -> s + shift); + } + + /** + * Performs the median and average component and returns a new sample. + * The sample's instant indicates the sourced data's center time, approximating how much + * latency was introduced by the filter. + */ + private Sample compute() { + int count = mWindow.size(); + if (count == 0) { + throw new IllegalStateException("The filter is empty."); + } + if (count == 1) { + return mWindow.getFirst(); + } + List<Sample> sorted = sortSamples(mWindow); + + if (mCut == 1F) { + // 100% of a median cut is just an average. + + // Note that this comes AFTER the sort. MARotationFilter's averaging routine + // requires that samples are sorted, as it sorts in a special way to respect angle + // rollover. + return averageSortedSamples(sorted); + } + + int throwAway = Math.round(count * (1 - mCut) / 2); + if (2 * throwAway >= count) { + // At least 2 samples if count is even or 1 sample if count is odd + throwAway--; + } + + return averageSortedSamples(sorted.subList(throwAway, count - throwAway)); + } + + /** + * Creates a new, sorted list containing the provided samples. Sorting is based on the sample + * value. + * @param list A list of samples to sort. + * @return A new list, of the same samples, sorted by value. + */ + protected List<Sample> sortSamples(Collection<Sample> list) { + ArrayList<Sample> sorted = new ArrayList<>(list); + Collections.sort(sorted); + return sorted; + } + + /** + * Averages a list of samples. + * @param samples The list of samples. + * @return A sample containing the average value and time of the samples in the list. + */ + protected Sample averageSortedSamples(Collection<Sample> samples) { + int size = samples.size(); + if (size == 0) { + return null; // Average can't be computed. + } + float valueSum = 0F; + + // Using a relevant epoch keeps the values small and therefore decreases the risk of + // overflow. + long instantEpoch = samples.stream().findFirst().get().instant.toEpochMilli(); + long instantSum = 0; + for (Sample s: samples) { + if (s == null) { + // there should never be a null. It's not worth decrementing size and checking for + // size == 0 again.lis + return null; // Average can't be computed. + } + valueSum += s.value; + instantSum += s.instant.toEpochMilli() - instantEpoch; + } + float avg = valueSum / size; + return new Sample( + avg, + Instant.ofEpochMilli(instantEpoch + instantSum / size)); + } + + /** + * This interface can be used to implement a remapper - a function that changes historical data + * in the filter in order to compensate for aspects of pose changes. + */ + public interface RemapFunction { + /** + * Performs a change to a data point in a filter. + * @param value The value that needs compensation from a pose change. + * @return The new, pose-compensated value. + */ + float run(float value); + } +} diff --git a/service/java/com/android/server/uwb/correction/filtering/MARotationFilter.java b/service/java/com/android/server/uwb/correction/filtering/MARotationFilter.java new file mode 100644 index 00000000..d404d218 --- /dev/null +++ b/service/java/com/android/server/uwb/correction/filtering/MARotationFilter.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.filtering; + +import static com.android.server.uwb.correction.math.MathHelper.F_PI; + +import com.android.server.uwb.correction.math.MathHelper; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * A median and average filter that operates identically to {@link MAFilter}, but uses the radians + * circular number system, wherein numbers refer to points on a circle such that +PI and -PI are + * the same, and averages refer to the average angle on a circle rather than the average of their + * linear numerical values. + */ +public class MARotationFilter extends MAFilter { + public MARotationFilter(int windowSize, float cut) { + super(windowSize, cut); + } + + /** + * Creates a naive average of the given samples. Both the value and instant of the samples are + * averaged. This will probably not produce a desired result if the samples are normalized + * to roll over at +/-PI rad. Use {@link #sortSamples(Collection)} to shift the roll over + * to a more desired location. + * @param samples The list of samples. + * @return The average of the samples, normalized within +/-PI. + */ + @Override + protected Sample averageSortedSamples(Collection<Sample> samples) { + Sample result = super.averageSortedSamples(samples); + return new Sample(MathHelper.normalizeRadians(result.value), result.instant); + } + + /** + * Rewrites all sample values based on the selector. + * @param selector The interface containing the remapping function. + */ + public void remap(RemapFunction selector) { + super.remap(v -> MathHelper.normalizeRadians(selector.run(v))); + } + + /** + * Changes the input list so that angles can be sorted, averaged and compared, even if + * they are on either side of the +/-180 divide. Note that this will return angles + * higher than 180 degrees. + * The input values must be between -180 and +180. + * Creating a sorted list, this finds the largest "gap" between angles and assumes that + * angles on either side of that gap represent the upper and lower bounds of what needs to + * be averaged. It then adds 360 to the values to the left of the gap and rearranges + * to make the list is sorted again. + * (Degrees are used for the explanation - this function actually operates on radians) + */ + @Override + protected List<Sample> sortSamples(Collection<Sample> list) { + List<Sample> sorted = super.sortSamples(list); + int size = sorted.size(); + if (size < 2) { + return sorted; + } + + // The initial gap to check, maybe not the biggest, is the gap on either side of +/-180, + // which is at the index 0. + int largestGapIndex = 0; + float largestGapSize = + (sorted.get(size - 1).value - sorted.get(0).value + 2 * F_PI) % F_PI; + for (int i = 1; i < size; i++) { + float diff = sorted.get(i).value - sorted.get(i - 1).value; + if (diff > largestGapSize) { + largestGapSize = diff; + largestGapIndex = i; + } + } + for (int i = 0; i < largestGapIndex; i++) { + sorted.set( + i, + new Sample(sorted.get(i).value + 2 * F_PI, sorted.get(i).instant) + ); + } + Collections.rotate(sorted, -largestGapIndex); + return sorted; + } +} diff --git a/service/java/com/android/server/uwb/correction/filtering/PositionFilterImpl.java b/service/java/com/android/server/uwb/correction/filtering/PositionFilterImpl.java new file mode 100644 index 00000000..aab30b32 --- /dev/null +++ b/service/java/com/android/server/uwb/correction/filtering/PositionFilterImpl.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.filtering; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.server.uwb.correction.math.Pose; +import com.android.server.uwb.correction.math.SphericalVector; +import com.android.server.uwb.correction.math.Vector3; +import com.android.server.uwb.correction.pose.IPoseSource; + +import java.time.Instant; +import java.util.Objects; + +/** + * An implementation of a combined azimuth, distance and elevation filter, which can be shifted + * as the pose changes. + * A filter that operates on X/Y/Z may be faster, but it would not support filtering differently + * for angle and distance. + */ +public class PositionFilterImpl implements IPositionFilter { + @NonNull private final IFilter mAzimuthFilter; + @NonNull private final IFilter mElevationFilter; + @NonNull private final IFilter mDistanceFilter; + private Pose mLastPose; + + public PositionFilterImpl( + @NonNull IFilter azimuthFilter, + @NonNull IFilter elevationFilter, + @NonNull IFilter distanceFilter) { + Objects.requireNonNull(azimuthFilter); + Objects.requireNonNull(elevationFilter); + Objects.requireNonNull(distanceFilter); + this.mAzimuthFilter = azimuthFilter; + this.mElevationFilter = elevationFilter; + this.mDistanceFilter = distanceFilter; + } + + /** + * Adds a value to the filter. + * + * @param value The value to add to the filter. + * @param instant When the value occurred, used to determine the latency introduced by + * the filter. Note that this has no effect on the order in which the filter + * operates + */ + @Override + public void add(@NonNull SphericalVector value, @NonNull Instant instant) { + Objects.requireNonNull(value); + Objects.requireNonNull(instant); + mAzimuthFilter.add(value.azimuth, instant); + mElevationFilter.add(value.elevation, instant); + mDistanceFilter.add(value.distance, instant); + } + + /** + * Computes a predicted UWB position based on the new pose. + * + * @param instant The instant for which the UWB prediction should be computed. + */ + @Override + public SphericalVector compute(@NonNull Instant instant) { + Objects.requireNonNull(instant); + // Cartesian extrapolation would happen here, such as target movement. + // Spherical extrapolation can happen in the filter because it operates on + // spherical values. + + return SphericalVector.fromRadians( + mAzimuthFilter.getResult(instant).value, + mElevationFilter.getResult(instant).value, + mDistanceFilter.getResult(instant).value + ); + } + + /** + * Updates the filter history to account for changes to the pose. Note that the entire + * pose source object is provided, so that its capabilities can be assessed as a part + * of the computation. + * + * @param poseSource The pose source that has the new pose. + */ + @Override + public void updatePose(@Nullable IPoseSource poseSource, @NonNull Instant instant) { + if (poseSource == null) { + return; + } + Pose newPose = poseSource.getPose(); + if (mLastPose != null && newPose != null && newPose != mLastPose) { + Pose deltaPose = Pose.compose(newPose.inverted(), mLastPose); + updatePoseFromDelta(deltaPose, compute(instant)); + } + mLastPose = newPose; + } + + /** + * Applies compensations to the azimuth, elevation and distance filters based on how the + * pose changed, and how the last-known position of the tag would be affected. + * + * @param deltaPose A relative transform describing how the pose changed. + * @param estimate The last known location of the UWB signal. + */ + private void updatePoseFromDelta(@NonNull Pose deltaPose, @NonNull SphericalVector estimate) { + // This conversion (Spherical -> Cartesian -> transform -> Spherical) is the best + // I have for right now. At the expense of readability, this could more efficiently + // transform spherical coordinates if performance is a problem. + + // Last known position of tag, relative to camera as of previous pose. + Vector3 vecFromOldCam = estimate.toCartesian(); + + // Convert to position of tag, relative to camera after the pose changed. + Vector3 vecFromNewCam = deltaPose.transformPoint(vecFromOldCam); + + // New azimuth, elevation and distance based on this new tag position. + SphericalVector newEstimate = SphericalVector.fromCartesian(vecFromNewCam); + + // Adjust the filters to represent this new estimation. + mAzimuthFilter.compensate(newEstimate.azimuth - estimate.azimuth); + mElevationFilter.compensate(newEstimate.elevation - estimate.elevation); + mDistanceFilter.compensate(newEstimate.distance - estimate.distance); + } +} diff --git a/service/java/com/android/server/uwb/correction/filtering/Sample.java b/service/java/com/android/server/uwb/correction/filtering/Sample.java new file mode 100644 index 00000000..68614867 --- /dev/null +++ b/service/java/com/android/server/uwb/correction/filtering/Sample.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.filtering; + +import androidx.annotation.NonNull; + +import java.time.Instant; + +/** + * Represents a data sample and when it was acquired. + */ +public class Sample implements Comparable<Sample> { + public float value; + public Instant instant; + + /** + * Creates a new instance of the Sample class. + * @param value The value of the sample. + * @param instant The time at which the value was relevant. + */ + Sample(float value, Instant instant) { + this.value = value; + this.instant = instant; + } + + /** + * Compares this sample to another, ignoring the time of the samples. + * @param other The other sample to compare to. + * @return a negative integer, zero, or a positive integer as this object + * is less than, equal to, or greater than the specified object. + * @throws NullPointerException if the specified object is null + */ + @Override + public int compareTo(@NonNull Sample other) { + return Float.compare(value, other.value); + } +} diff --git a/service/java/com/android/server/uwb/correction/math/AoAVector.java b/service/java/com/android/server/uwb/correction/math/AoAVector.java index 85e62a1d..01010d8d 100644 --- a/service/java/com/android/server/uwb/correction/math/AoAVector.java +++ b/service/java/com/android/server/uwb/correction/math/AoAVector.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,18 +42,18 @@ import java.util.Objects; /** * Represents a point in space as distance, azimuth and elevation. * This uses OpenGL's right-handed coordinate system, where the origin is facing in the - * -Z direction. Increasing azimuth rotates around Y and increases X. Increasing - * elevation rotates around X and increases Y. + * -Z direction. Increasing azimuth rotates around Y and increases X. Increasing + * elevation rotates around X and increases Y. * * Note that this is NOT quite a spherical vector. It represents angles seen by AoA antennas. * In this implementation, azimuth and elevation are treated the same. Therefore, for example: - * Very "up" or "down" targets will have an azimuth near 0, because the signal will arrive at - * both AoA antennas at nearly the same time. + * Very "up" or "down" targets will have an azimuth near 0, because the signal will arrive at + * both AoA antennas at nearly the same time. * In a spherical vector, azimuth is computed exclusively from the horizontal plane and treated - * independently of the vertical axis, but elevation is computed along the plane of the azimuth. + * independently of the vertical axis, but elevation is computed along the plane of the azimuth. * This also means that there are some angles that are impossible. For example, something with - * a 90deg azimuth (directly right of the phone) cannot possibly be viewed by the elevation - * antennas from any angle other than 0deg. + * a 90deg azimuth (directly right of the phone) cannot possibly be viewed by the elevation + * antennas from any angle other than 0deg. */ @Immutable public final class AoAVector { @@ -64,8 +64,8 @@ public final class AoAVector { /** * Creates a AoAVector from the azimuth, elevation and distance of a viewpoint that is - * facing into the -Z axis. Illegal azimuth and elevation combinations will be scaled away - * from +/-90deg such that they are legal. + * facing into the -Z axis. Illegal azimuth and elevation combinations will be scaled away + * from +/-90deg such that they are legal. * * @param azimuth The angle along the X axis, around the Y axis. * @param elevation The angle along the Y axis, around the X axis. @@ -76,7 +76,7 @@ public final class AoAVector { float ae = abs(elevation); if (ae > F_HALF_PI) { // Normalize elevation to be only +/-90 - if it's outside that, mirror and bound the - // elevation and flip the azimuth. + // elevation and flip the azimuth. elevation = (F_PI - ae) * signum(elevation); azimuth += F_PI; } @@ -97,11 +97,11 @@ public final class AoAVector { float scaleFactor = angleSum / (F_HALF_PI); if (scaleFactor > 1) { // The combination of degrees isn't possible - for example, the azimuth suggests that - // the target is exactly 90deg to the right, and yet elevation is non-zero. + // the target is exactly 90deg to the right, and yet elevation is non-zero. // The elevation and azimuth will be scaled down until they are within - // legal limits. This will create a bias away from 90-degree readings. - // Note that azimuth will be corrected to higher than 90deg if it was originally - // above 90deg. + // legal limits. This will create a bias away from 90-degree readings. + // Note that azimuth will be corrected to higher than 90deg if it was originally + // above 90deg. elevation /= scaleFactor; azimuth = backFacing ? (F_PI * signum(azimuth) - laz / scaleFactor) : (azimuth / scaleFactor); @@ -159,7 +159,7 @@ public final class AoAVector { /** * Produces an AoA vector from a cartesian vector, converting X, Y and Z values to - * azimuth, elevation and distance. + * azimuth, elevation and distance. * * @param position The cartesian representation to convert. * @return An equivalent AoA vector representation. @@ -172,7 +172,7 @@ public final class AoAVector { /** * Produces a AoA vector from a cartesian vector, converting X, Y and Z values to - * azimuth, elevation and distance. + * azimuth, elevation and distance. * * @param x The cartesian x-coordinate to convert. * @param y The cartesian y-coordinate to convert. diff --git a/service/java/com/android/server/uwb/correction/math/MathHelper.java b/service/java/com/android/server/uwb/correction/math/MathHelper.java index be5f79de..dae0e2f0 100644 --- a/service/java/com/android/server/uwb/correction/math/MathHelper.java +++ b/service/java/com/android/server/uwb/correction/math/MathHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ public final class MathHelper { /** * Converts degrees that may be outside +/-180 to an equivalent rotation value between - * -180 (excl) and 180 (incl). + * -180 (excl) and 180 (incl). * @param deg The degrees to normalize * @return A value above -180 and up to 180 that has an equivalent angle to the input. */ diff --git a/service/java/com/android/server/uwb/correction/math/Matrix.java b/service/java/com/android/server/uwb/correction/math/Matrix.java index 7e0746f6..bf64b799 100644 --- a/service/java/com/android/server/uwb/correction/math/Matrix.java +++ b/service/java/com/android/server/uwb/correction/math/Matrix.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/service/java/com/android/server/uwb/correction/math/Pose.java b/service/java/com/android/server/uwb/correction/math/Pose.java index 4e99ccab..a15b243e 100644 --- a/service/java/com/android/server/uwb/correction/math/Pose.java +++ b/service/java/com/android/server/uwb/correction/math/Pose.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ public class Pose { * * @param translation a {@code float[3]} representing the translation vector * @param rotation a {@code float[4]} representing the rotation quaternion following the - * Hamilton convention. + * Hamilton convention. * @throws IllegalArgumentException if translation and rotation lengths are wrong. */ public Pose(float[] translation, float[] rotation) { @@ -99,9 +99,9 @@ public class Pose { } /** - * Transforms the provided point by the pose. This converts a point relative to the pose into - * a world-relative point. To convert from a world-relative point to a pose-relative point, - * use pose.inverted().transformPoint(point) + * Transforms the provided point by the pose. This converts a point relative to the pose into + * a world-relative point. To convert from a world-relative point to a pose-relative point, + * use pose.inverted().transformPoint(point) */ public Vector3 transformPoint(Vector3 point) { return rotation.rotateVector(point).add(translation); diff --git a/service/java/com/android/server/uwb/correction/math/Quaternion.java b/service/java/com/android/server/uwb/correction/math/Quaternion.java index efbd8356..f649911d 100644 --- a/service/java/com/android/server/uwb/correction/math/Quaternion.java +++ b/service/java/com/android/server/uwb/correction/math/Quaternion.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,8 @@ import java.util.Locale; * Represents an orientation in 3D space. * * This uses OpenGL's right-handed coordinate system, where the origin is facing in the - * -Z direction. Angle operations such as {@link Quaternion#yawPitchRoll(float, float, float)} - * assume these operations relative to a quaternion facing in the -Z direction. + * -Z direction. Angle operations such as {@link Quaternion#yawPitchRoll(float, float, float)} + * assume these operations relative to a quaternion facing in the -Z direction. * * +Y * | -Z @@ -48,8 +48,8 @@ import java.util.Locale; * -Y * * Yaw, pitch and roll direction can be determined by "grabbing" the axis you're rotating with - * your right hand, orienting your thumb to point in the positive direction. Your fingers' curl - * direction indicates the rotation created by positive numbers. + * your right hand, orienting your thumb to point in the positive direction. Your fingers' curl + * direction indicates the rotation created by positive numbers. */ @SuppressWarnings("UnaryPlus") @Immutable diff --git a/service/java/com/android/server/uwb/correction/math/SphericalVector.java b/service/java/com/android/server/uwb/correction/math/SphericalVector.java index acb6412d..c580a881 100644 --- a/service/java/com/android/server/uwb/correction/math/SphericalVector.java +++ b/service/java/com/android/server/uwb/correction/math/SphericalVector.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,8 +41,8 @@ import java.util.Objects; /** * Represents a point in space represented as distance, azimuth and elevation. * This uses OpenGL's right-handed coordinate system, where the origin is facing in the - * -Z direction. Increasing azimuth rotates around Y and increases X. Increasing - * elevation rotates around X and increases Y. + * -Z direction. Increasing azimuth rotates around Y and increases X. Increasing + * elevation rotates around X and increases Y. */ @Immutable public class SphericalVector { @@ -52,7 +52,7 @@ public class SphericalVector { /** * Creates a SphericalVector from the azimuth, elevation and distance of a viewpoint that is - * facing into the -Z axis. + * facing into the -Z axis. * * @param azimuth The angle along the X axis, around the Y axis. * @param elevation The angle along the Y axis, around the X axis. @@ -63,7 +63,7 @@ public class SphericalVector { float ae = abs(elevation); if (ae > F_HALF_PI) { // Normalize elevation to be only +/-90 - if it's outside that, mirror and bound the - // elevation and flip the azimuth. + // elevation and flip the azimuth. elevation = (F_PI - ae) * signum(elevation); azimuth += F_PI; } @@ -119,7 +119,7 @@ public class SphericalVector { /** * Produces a SphericalVector from a cartesian vector, converting X, Y and Z values to - * azimuth, elevation and distance. + * azimuth, elevation and distance. * * @param position The cartesian representation to convert. * @return An equivalent spherical vector representation. @@ -132,7 +132,7 @@ public class SphericalVector { /** * Produces a spherical vector from a cartesian vector, converting X, Y and Z values to - * azimuth, elevation and distance. + * azimuth, elevation and distance. * * @param x The cartesian x-coordinate to convert. * @param y The cartesian y-coordinate to convert. @@ -208,7 +208,7 @@ public class SphericalVector { /** * Converts this SphericalVector to an equivalent sparse Spherical Vector that has all 3 - * components. + * components. * * @return An equivalent {@link Sparse}. */ @@ -218,7 +218,7 @@ public class SphericalVector { /** * Converts this SphericalVector to an equivalent sparse Spherical Vector, with the specified - * presence or absence of values. + * presence or absence of values. * * @param hasAzimuth True if the vector includes azimuth. * @param hasElevation True if the vector includes elevation. diff --git a/service/java/com/android/server/uwb/correction/math/Vector3.java b/service/java/com/android/server/uwb/correction/math/Vector3.java index 894db9db..6ea3367d 100644 --- a/service/java/com/android/server/uwb/correction/math/Vector3.java +++ b/service/java/com/android/server/uwb/correction/math/Vector3.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -131,8 +131,8 @@ public class Vector3 { /** * Gets the square of the length of the vector. When performing length comparisons, - * it is more optimal to compare against a squared length to avoid having to perform - * a sqrt. + * it is more optimal to compare against a squared length to avoid having to perform + * a sqrt. * @return The square of the length of the vector. */ public float lengthSquared() { @@ -206,7 +206,7 @@ public class Vector3 { /** * Converts a vector expressed in radians (ie - yaw, pitch, roll), into degrees. Primarily - * used as a convenience to display data to a user. + * used as a convenience to display data to a user. * @return A Vector3 multiplied by 180/PI. */ public Vector3 toDegrees() { diff --git a/service/java/com/android/server/uwb/correction/pose/GyroPoseSource.java b/service/java/com/android/server/uwb/correction/pose/GyroPoseSource.java new file mode 100644 index 00000000..b80bc736 --- /dev/null +++ b/service/java/com/android/server/uwb/correction/pose/GyroPoseSource.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.pose; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.server.uwb.correction.math.MathHelper; +import com.android.server.uwb.correction.math.Pose; +import com.android.server.uwb.correction.math.Quaternion; +import com.android.server.uwb.correction.math.Vector3; + +import java.security.InvalidParameterException; +import java.time.Instant; +import java.util.EnumSet; +import java.util.Objects; + +/** + * Provides poses from the phone's gyro, which provides relative changes to yaw, pitch and roll. + * Positional changes are not supported. Note that this pose source has many limitations, + * particularly because it drifts and has no sense of down. It is likely to produce weak results + * as the phone rotates, and non-elevation phones cannot estimate elevation because there is no + * absolute sense of pitch from this pose source. + * + * The only reason to use this class is to save a very, very marginal amount of power by not using + * the accelerometer (to make pitch and roll absolute) and the magnetometer (to make yaw absolute), + * or if those sensors do not exist. + * + * Consider using the {@link RotationPoseSource}. + */ +public class GyroPoseSource extends PoseSourceBase implements SensorEventListener { + private static final String TAG = "GyroPoseSource"; + private final SensorManager mSensorManager; + private final Sensor mSensor; + private final int mIntervalUs; + private final int mIntervalMs; + + float mAbsoluteYaw = 0; + float mAbsolutePitch = 0; + float mAbsoluteRoll = 0; + + private long mLastUpdate; + + /** + * Creates a new instance of the GyroPoseSource + * @param intervalMs How frequently to update the pose. + */ + public GyroPoseSource(@NonNull Context context, int intervalMs) + throws UnsupportedOperationException { + Objects.requireNonNull(context); + if (intervalMs < MIN_INTERVAL_MS || intervalMs > MAX_INTERVAL_MS) { + throw new InvalidParameterException("Invalid interval."); + } + mSensorManager = context.getSystemService(SensorManager.class); + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + if (mSensor == null) { + throw new UnsupportedOperationException("Device does not support the gyroscope."); + } + this.mIntervalMs = intervalMs; + mIntervalUs = intervalMs * 1000; + } + + /** + * {@inheritDoc} + */ + @Override + protected void start() { + mSensorManager.registerListener(this, mSensor, mIntervalUs); + } + + /** + * {@inheritDoc} + */ + @Override + protected void stop() { + mSensorManager.unregisterListener(this); + } + + /** + * {@inheritDoc} + */ + @Override + public void onSensorChanged(SensorEvent event) { + // Yaw changes are relative to the phone, but rotation vector yaw changes are relative to + // to the world. + // Pitch and roll might just spin forever due to drift; this filter might actually + // be more useful if it only produced yaw values. + // Further, because the data is accumulated as YPR instead of a Quaternion, there may + // be strange gimbal side-effects. + if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) { + long now = Instant.now().toEpochMilli(); + long timeSpan = now - mLastUpdate; + + mLastUpdate = now; + if (timeSpan > mIntervalMs * 2L) { + // Keep a limit on how long we'll integrate motion. + timeSpan = mIntervalMs; + } + + // Compute az/el absolute change over the course of the sample. + float yaw = event.values[1] * (timeSpan / 1000F); + float pitch = event.values[0] * (timeSpan / 1000F); + float roll = event.values[2] * (timeSpan / 1000F); + + mAbsoluteYaw = MathHelper.normalizeRadians(mAbsoluteYaw + yaw); + mAbsolutePitch = MathHelper.normalizeRadians(mAbsolutePitch + pitch); + mAbsoluteRoll = MathHelper.normalizeRadians(mAbsoluteRoll + roll); + + publish(new Pose( + Vector3.ORIGIN, + Quaternion.yawPitchRoll(mAbsoluteYaw, mAbsolutePitch, mAbsoluteRoll) + )); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + Log.d(TAG, "onAccuracyChanged() $sensor"); + } + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public EnumSet<Capabilities> getCapabilities() { + return Capabilities.ROTATION; + } +} diff --git a/service/java/com/android/server/uwb/correction/pose/IPoseSource.java b/service/java/com/android/server/uwb/correction/pose/IPoseSource.java new file mode 100644 index 00000000..274f2aed --- /dev/null +++ b/service/java/com/android/server/uwb/correction/pose/IPoseSource.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.pose; + +import androidx.annotation.NonNull; + +import com.android.server.uwb.correction.math.Pose; + +import java.util.EnumSet; + +/** + * Provides pose update information and a way for subscribers to listen for them. + */ +public interface IPoseSource extends AutoCloseable { + /** The shortest practical update interval for a pose source. */ + int MIN_INTERVAL_MS = 1000 / 60; // 60Hz + + /** The longest practical update interval for a pose source. */ + int MAX_INTERVAL_MS = 10000; // 0.1Hz. + + /** + * A set of all possible pose source capabilities. + */ + enum Capabilities { + YAW, PITCH, ROLL, X, Y, Z, + /** + * Indicates that a pitch and roll of 0 means that the phone is upright. If this flag + * is not present, pitch and roll changes are only relative. + */ + UPRIGHT; + + public static final EnumSet<Capabilities> ALL = EnumSet.allOf(Capabilities.class); + public static final EnumSet<Capabilities> NONE = EnumSet.noneOf(Capabilities.class); + public static final EnumSet<Capabilities> ROTATION = EnumSet.of( + Capabilities.YAW, + Capabilities.PITCH, + Capabilities.ROLL + ); + public static final EnumSet<Capabilities> UPRIGHT_ROTATION = EnumSet.of( + Capabilities.YAW, + Capabilities.PITCH, + Capabilities.ROLL, + Capabilities.UPRIGHT); + public static final EnumSet<Capabilities> TRANSLATION = EnumSet.of( + Capabilities.X, + Capabilities.Y, + Capabilities.Z); + } + + /** + * Stops the pose sensing and removes all listeners. + */ + @Override + void close(); + + /** + * Registers a listener for the pose updates. + * @param listener The PoseEventListener that will be notified when the pose changes. + */ + void registerListener(@NonNull PoseEventListener listener); + + /** + * Unregisters a listener from the pose updates. + * @param listener The PoseEventListener that will no longer be notified when the pose changes. + * @return True if successfully removed. Note that a listener may be prematurely removed if it + * has thrown an error. + */ + boolean unregisterListener(@NonNull PoseEventListener listener); + + /** + * Gets the current pose. + * @return The current pose. May be null. + */ + Pose getPose(); + + /** + * Gets the capabilities of this pose source. + * @return An EnumSet of Capabilities. + */ + @NonNull + EnumSet<Capabilities> getCapabilities(); +} diff --git a/service/java/com/android/server/uwb/correction/pose/IntegPoseSource.java b/service/java/com/android/server/uwb/correction/pose/IntegPoseSource.java new file mode 100644 index 00000000..7dec772f --- /dev/null +++ b/service/java/com/android/server/uwb/correction/pose/IntegPoseSource.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.pose; + +import static com.android.server.uwb.correction.math.MathHelper.F_HALF_PI; + +import static java.lang.Math.abs; +import static java.lang.Math.min; +import static java.lang.Math.signum; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.server.uwb.correction.math.Pose; +import com.android.server.uwb.correction.math.Quaternion; +import com.android.server.uwb.correction.math.Vector3; + +import java.security.InvalidParameterException; +import java.time.Instant; +import java.util.EnumSet; +import java.util.Objects; + +/** + * Provides poses by double-integrating the accelerometer. It is hilariously bad with ordinary + * accelerometers. + * The rotation sensor is used for rotation. + * + * Use this pose source if your device has a military-grade accelerometer, or build upon this + * class to research better double-integration technology such as AI-based double integration, or + * ARCore pose tracking. + */ +public class IntegPoseSource extends PoseSourceBase implements SensorEventListener { + private static final String TAG = "IntegPoseSource"; + private final SensorManager mSensorManager; + private final Sensor mRotationSensor; + private final Sensor mAccelSensor; + private final int mInterval; + private Vector3 mAccelCal = new Vector3(0, 0, 0); + private Vector3 mPosition = new Vector3(0, 0, 0); + private Vector3 mSpeed = new Vector3(0, 0, 0); + private long mLastUpdate; + + /** how much drift from origin before position is reset to the origin. In meters. */ + final int mPosResetDistance = 20; + final float mSpeedDamp = 0.95F; // Speed coefficient (1=no slowing, 0.999F=some slowing) + final float mPosDamp = 0.999F; // Position recentering (1=no recentering, 0.999F=some) + final float mCalRate = 0.002F; // How quickly calibration adjusts to accel changes. + + // The local system is oriented with Y up. The Android rotation vector has Z up. Pitching down + // will correct this. + private final Quaternion mRotator = Quaternion.yawPitchRoll(0, -F_HALF_PI, 0); + + /** + * Creates a new instance of the IntegPoseSource + * @param intervalMs How frequently to update the pose. + */ + public IntegPoseSource(@NonNull Context context, int intervalMs) { + Objects.requireNonNull(context); + if (intervalMs < MIN_INTERVAL_MS || intervalMs > MAX_INTERVAL_MS) { + throw new InvalidParameterException("Invalid interval."); + } + mSensorManager = context.getSystemService(SensorManager.class); + mRotationSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); + if (mRotationSensor == null) { + throw new UnsupportedOperationException( + "Device does not support the required rotation vector sensors."); + } + mAccelSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION); + if (mAccelSensor == null) { + throw new UnsupportedOperationException( + "Device does not support the required linear acceleration sensors." + ); + } + mInterval = intervalMs * 1000; + } + + /** + * {@inheritDoc} + */ + @Override + protected void start() { + mSensorManager.registerListener(this, mRotationSensor, mInterval); + mSensorManager.registerListener(this, mAccelSensor, mInterval); + } + + /** + * {@inheritDoc} + */ + @Override + protected void stop() { + mSensorManager.unregisterListener(this); + } + + /** + * {@inheritDoc} + */ + @Override + public void onSensorChanged(SensorEvent event) { + if (event.sensor.getType() == Sensor.TYPE_LINEAR_ACCELERATION) { + if (mLastUpdate == 0) { + mLastUpdate = Instant.now().toEpochMilli(); + return; + } + long now = Instant.now().toEpochMilli(); + float dur = (now - mLastUpdate) / 1000.0F; + mLastUpdate = now; + Vector3 accel = new Vector3( + event.values[0] - mAccelCal.x, + event.values[1] - mAccelCal.y, + event.values[2] - mAccelCal.z + ); + mAccelCal = new Vector3( + mAccelCal.x + min(abs(accel.x), mCalRate) * signum(accel.x), + mAccelCal.y + min(abs(accel.y), mCalRate) * signum(accel.y), + mAccelCal.z + min(abs(accel.z), mCalRate) * signum(accel.z) + ); + mSpeed = new Vector3( + (mSpeed.x + accel.x * dur) * mSpeedDamp, + (mSpeed.y + accel.y * dur) * mSpeedDamp, + (mSpeed.z + accel.z * dur) * mSpeedDamp + ); + mPosition = new Vector3( + (mPosition.x + mSpeed.x * dur) * mPosDamp, + (mPosition.y + mSpeed.y * dur) * mPosDamp, + (mPosition.z + mSpeed.z * dur) * mPosDamp + ); + if (mPosition.lengthSquared() > mPosResetDistance * mPosResetDistance) { + mPosition = Vector3.ORIGIN; + } + } else if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) { + Quaternion base = new Quaternion( + event.values[0], + event.values[1], + event.values[2], + event.values[3] + ); + publish(new Pose(mPosition, Quaternion.multiply(mRotator, base))); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + Log.d(TAG, "onAccuracyChanged() $sensor"); + } + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public EnumSet<Capabilities> getCapabilities() { + return Capabilities.ALL; + } +} diff --git a/service/java/com/android/server/uwb/correction/pose/PoseEventListener.java b/service/java/com/android/server/uwb/correction/pose/PoseEventListener.java new file mode 100644 index 00000000..3447988b --- /dev/null +++ b/service/java/com/android/server/uwb/correction/pose/PoseEventListener.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.pose; + +import androidx.annotation.NonNull; + +import com.android.server.uwb.correction.math.Pose; + +/** + * Used for receiving notifications from a PoseSource when there is new pose data. + */ +public interface PoseEventListener { + /** + * Called when there is an update to the device's pose. The origin is arbitrary, but + * position could be relative to the starting position, and rotation could be relative + * to magnetic north and the direction of gravity. + * @param pose The new location and orientation of the device. + */ + void onPoseChanged(@NonNull Pose pose); +} diff --git a/service/java/com/android/server/uwb/correction/pose/PoseSourceBase.java b/service/java/com/android/server/uwb/correction/pose/PoseSourceBase.java new file mode 100644 index 00000000..e446114b --- /dev/null +++ b/service/java/com/android/server/uwb/correction/pose/PoseSourceBase.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.pose; + +import android.util.Log; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; + +import com.android.server.uwb.correction.math.Pose; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Optional base implementation for a PoseSource. Provides help to register listeners and + * publishing. + */ +public abstract class PoseSourceBase implements IPoseSource { + private final Lock mLockObject = new ReentrantLock(); + @GuardedBy("mLockObject") + private final List<PoseEventListener> mListeners; + private static final String TAG = "PoseSourceBase"; + private final AtomicReference<Pose> mPose = new AtomicReference<>(); + + public PoseSourceBase() { + mListeners = new ArrayList<>(); + } + + /** + * Starts the pose source. Called by the {@link PoseSourceBase} when the first + * listener subscribes. + */ + protected abstract void start(); + + /** + * Stops the pose source. Called by the {@link PoseSourceBase} when the last + * listener unsubscribes. + */ + protected abstract void stop(); + + /** + * {@inheritDoc} + */ + @Override + public void close() { + + } + + /** + * {@inheritDoc} + */ + @Override + public void registerListener(@NonNull PoseEventListener listener) { + Objects.requireNonNull(listener); + mLockObject.lock(); + try { + mListeners.add(listener); + if (mListeners.size() == 1) { + start(); // Run inside the lock to make sure starts and stops are sequential. + } + } finally { + mLockObject.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean unregisterListener(@NonNull PoseEventListener listener) { + Objects.requireNonNull(listener); + mLockObject.lock(); + try { + boolean removed = mListeners.remove(listener); + if (removed && mListeners.size() == 0) { + stop(); // Run inside the lock to make sure starts and stops are sequential. + } + return removed; + } finally { + mLockObject.unlock(); + } + } + + /** + * Publishes the pose to all listeners. + * + * @param pose The updated device pose. + */ + protected void publish(@NonNull Pose pose) { + Objects.requireNonNull(pose); + ArrayList<PoseEventListener> listeners; + mLockObject.lock(); + try { + // Copy snapshot to minimize lock time and allow changes to listeners while + // we report pose changes. + listeners = new ArrayList<>(this.mListeners); + } finally { + mLockObject.unlock(); + } + this.mPose.set(pose); + for (int i = 0; i < listeners.size(); i++) { + try { + listeners.get(i).onPoseChanged(pose); + } catch (Exception ex) { + Log.e(TAG, ex.toString()); + + // Remove the listener, so it doesn't become a persistent problem. + listeners.remove(i--); + } + } + } + + @Override + public Pose getPose() { + return mPose.get(); + } +} diff --git a/service/java/com/android/server/uwb/correction/pose/RotationPoseSource.java b/service/java/com/android/server/uwb/correction/pose/RotationPoseSource.java new file mode 100644 index 00000000..45ef7825 --- /dev/null +++ b/service/java/com/android/server/uwb/correction/pose/RotationPoseSource.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.pose; + +import static com.android.server.uwb.correction.math.MathHelper.F_HALF_PI; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.server.uwb.correction.math.Pose; +import com.android.server.uwb.correction.math.Quaternion; +import com.android.server.uwb.correction.math.Vector3; + +import java.security.InvalidParameterException; +import java.util.EnumSet; +import java.util.Objects; + +/** + * Provides poses from the phone's rotation vector, which provides yaw, pitch and roll, + * oriented where +Y is north and +Z is skyward. Positional changes are not supported. + * The output value is rotated to the local system's orientation, where +Y is skyward. + * + * This pose source is very reliable and available on almost all phones, but provides no + * positioning. + */ +public class RotationPoseSource extends PoseSourceBase implements SensorEventListener { + private static final String TAG = "RotationPoseSource"; + private final SensorManager mSensorManager; + private final Sensor mSensor; + private final int mInterval; + + // The local system is oriented with Y up. The Android rotation vector has Z up. Pitching down + // will correct this. + private final Quaternion mRotator = Quaternion.yawPitchRoll(0, -F_HALF_PI, 0); + + /** + * Creates a new instance of the RotationPoseSource + * @param intervalMs How frequently to update the pose. + */ + public RotationPoseSource(@NonNull Context context, int intervalMs) { + Objects.requireNonNull(context); + if (intervalMs < MIN_INTERVAL_MS || intervalMs > MAX_INTERVAL_MS) { + throw new InvalidParameterException("Invalid interval."); + } + mSensorManager = context.getSystemService(SensorManager.class); + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); + if (mSensor == null) { + throw new UnsupportedOperationException("Device does not support the rotation vector."); + } + mInterval = intervalMs * 1000; + } + + /** + * {@inheritDoc} + */ + @Override + protected void start() { + mSensorManager.registerListener(this, mSensor, mInterval); + } + + /** + * {@inheritDoc} + */ + @Override + protected void stop() { + mSensorManager.unregisterListener(this); + } + + /** + * {@inheritDoc} + */ + @Override + public void onSensorChanged(SensorEvent event) { + if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) { + // The rotation vector is a quaternion oriented to gravity and geomagnetic north. + Quaternion base = new Quaternion( + event.values[0], + event.values[1], + event.values[2], + event.values[3] + ); + publish(new Pose(Vector3.ORIGIN, Quaternion.multiply(mRotator, base))); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + Log.d(TAG, "onAccuracyChanged() $sensor"); + } + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public EnumSet<Capabilities> getCapabilities() { + return Capabilities.UPRIGHT_ROTATION; + } +} diff --git a/service/java/com/android/server/uwb/correction/pose/SixDOFPoseSource.java b/service/java/com/android/server/uwb/correction/pose/SixDOFPoseSource.java new file mode 100644 index 00000000..ac80feaf --- /dev/null +++ b/service/java/com/android/server/uwb/correction/pose/SixDOFPoseSource.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.pose; + +import static com.android.server.uwb.correction.math.MathHelper.F_HALF_PI; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.server.uwb.correction.math.Pose; +import com.android.server.uwb.correction.math.Quaternion; +import com.android.server.uwb.correction.math.Vector3; + +import java.security.InvalidParameterException; +import java.util.EnumSet; +import java.util.Objects; + +/** + * Provides poses from the device's 6DOF fused sensor, which provides a full position and rotation + * relative to an arbitrary origin. + * + * This virtual sensor is usually only implemented in purpose-built spatial-tracking systems such as + * Google Glass and Meta Quest. It can be power-hungry. + * + * Functionally this resembles ARCore's pose tracking, but relieves the difficulty of having to get + * ARCore pose updates from the user application. + */ +public class SixDOFPoseSource extends PoseSourceBase implements SensorEventListener { + private static final String TAG = "SixDOFPoseSource"; + private final SensorManager mSensorManager; + private final Sensor mSensor; + private final int mInterval; + + // The local system is oriented with Y up. The Android rotation vector has Z up. Pitching down + // will correct this. + private final Quaternion mRotator = Quaternion.yawPitchRoll(0, -F_HALF_PI, 0); + + /** + * Creates a new instance of the FusionPoseSource. + * @param intervalMs How frequently to update the pose. + */ + public SixDOFPoseSource(@NonNull Context context, int intervalMs) { + Objects.requireNonNull(context); + if (intervalMs < MIN_INTERVAL_MS || intervalMs > MAX_INTERVAL_MS) { + throw new InvalidParameterException("Invalid interval."); + } + mSensorManager = context.getSystemService(SensorManager.class); + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_POSE_6DOF); + if (mSensor == null) { + throw new UnsupportedOperationException( + "Device does not support the Pose 6DOF sensor." + ); + } + mInterval = intervalMs * 1000; + } + + /** + * {@inheritDoc} + */ + @Override + protected void start() { + mSensorManager.registerListener(this, mSensor, mInterval); + } + + /** + * {@inheritDoc} + */ + @Override + protected void stop() { + mSensorManager.unregisterListener(this); + } + + /** + * {@inheritDoc} + */ + @Override + public void onSensorChanged(SensorEvent event) { + if (event.sensor.getType() == Sensor.TYPE_POSE_6DOF) { + // The rotation vector is a quaternion oriented to gravity and geomagnetic north. + // See https://developer.android.com/reference/android/hardware/Sensor#TYPE_POSE_6DOF + + // The local system is oriented with Y up. The Android position vector has Z up. + Vector3 position = new Vector3( + event.values[4], + event.values[6], // Y and Z swapped + event.values[5] + ); + + Quaternion rotation = new Quaternion( + event.values[0], + event.values[1], + event.values[2], + event.values[3] + ); + publish(new Pose(position, Quaternion.multiply(mRotator, rotation))); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + Log.d(TAG, "onAccuracyChanged() $sensor"); + } + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public EnumSet<Capabilities> getCapabilities() { + return Capabilities.ALL; + } +} diff --git a/service/java/com/android/server/uwb/correction/primers/AoAPrimer.java b/service/java/com/android/server/uwb/correction/primers/AoAPrimer.java new file mode 100644 index 00000000..9a67d068 --- /dev/null +++ b/service/java/com/android/server/uwb/correction/primers/AoAPrimer.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.primers; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.server.uwb.correction.math.AoAVector; +import com.android.server.uwb.correction.math.SphericalVector; +import com.android.server.uwb.correction.pose.IPoseSource; + +/** + * Converts a PDoA azimuth value to a spherical coordinate azimuth by accounting for elevation. + * See {@link AoAVector} for information on the difference. + * This primer is needed on hardware that does not support elevation, after the ElevationPrimer, + * so that the estimated elevation can be used to perform the PDoA-to-azimuth conversion. + * This primer is also needed on hardware that supports elevation, but with firmware that does + * not perform the PDoA-to-azimuth conversion. + */ +public class AoAPrimer implements IPrimer { + /** + * Applies corrections to a raw position. + * + * @param input The original UWB reading. + * @param prediction A prediction of where the signal probably came from. + * @param poseSource A pose source that may indicate phone orientation. + * @return A replacement value for the UWB input that has been corrected for the situation. + */ + @Override + public SphericalVector.Sparse prime( + @NonNull SphericalVector.Sparse input, + @Nullable SphericalVector prediction, + @Nullable IPoseSource poseSource) { + if (input.hasElevation && input.hasAzimuth) { + // Reinterpret the SphericalVector as an AoAVector, then convert it to a + // SphericalVector. + return AoAVector.fromRadians( + input.vector.azimuth, + input.vector.elevation, + input.vector.distance) + .toSphericalVector() + .toSparse(true, true, input.hasDistance); + } + return input; + } +} diff --git a/service/java/com/android/server/uwb/correction/primers/ElevationPrimer.java b/service/java/com/android/server/uwb/correction/primers/ElevationPrimer.java new file mode 100644 index 00000000..6203c727 --- /dev/null +++ b/service/java/com/android/server/uwb/correction/primers/ElevationPrimer.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.primers; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.server.uwb.correction.math.Pose; +import com.android.server.uwb.correction.math.SphericalVector; +import com.android.server.uwb.correction.pose.IPoseSource; +import com.android.server.uwb.correction.pose.IPoseSource.Capabilities; + +/** + * Applies a default pose-based elevation to a UWB reading. A basic "assumption" about what the + * elevation might be helps improve the quality of pose-based azimuth compensations, and may + * provide a more understandable UWB location guess to the user. + * Recommended for hardware that does not support elevation. This should execute before the + * AoAPrimer in the primer execution order. + */ +public class ElevationPrimer implements IPrimer { + /** + * Applies a default pose-based elevation to a UWB reading that doesn't have one. + * + * @param input The original UWB reading. + * @param prediction A prediction of where the signal probably came from. + * @param poseSource A pose source that may indicate phone orientation. + * @return A replacement value for the UWB vector that has been corrected for the situation. + */ + @Override + public SphericalVector.Sparse prime( + @NonNull SphericalVector.Sparse input, + @Nullable SphericalVector prediction, + @Nullable IPoseSource poseSource) { + // Early exit: If there is already an elevation, we won't try to fill it in. + if (input.hasElevation) { + return input; + } + + SphericalVector.Sparse position = input; + if (poseSource != null + && poseSource.getCapabilities().contains(Capabilities.UPRIGHT) + ) { + Pose pose = poseSource.getPose(); + if (pose != null) { + // The pose source knows which way is upright, so if we don't have + // an AoA elevation, we'll assume that elevation is level with the phone. + // i.e. If the phone pitches down, the elevation would appear up. + + position = new SphericalVector.Sparse( + SphericalVector.fromRadians( + input.vector.azimuth, + -pose.rotation.toYawPitchRoll().y, // -Pitch becomes our assumed elevation + input.vector.distance + ), + input.hasAzimuth, + true, + input.hasDistance + ); + } + } + return position; + } +} diff --git a/service/java/com/android/server/uwb/correction/primers/FovPrimer.java b/service/java/com/android/server/uwb/correction/primers/FovPrimer.java new file mode 100644 index 00000000..035df1cd --- /dev/null +++ b/service/java/com/android/server/uwb/correction/primers/FovPrimer.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.primers; + +import static com.android.server.uwb.correction.math.MathHelper.F_PI; + +import static java.lang.Math.abs; +import static java.lang.Math.cos; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.server.uwb.correction.math.SphericalVector; +import com.android.server.uwb.correction.pose.IPoseSource; + +/** + * Limits the field view of incoming UWB readings by replacing angles outside the defined limits + * with predicted angles (which are based on the last-known-good angle combined with pose changes). + * + * Most UWB hardware suffers from accuracy issues beyond a certain azimuth or elevation, and + * conversely will produce erroneous steep angles when there are issues with the signal. + * + * This implementation imposes a double-cone-shaped FOV, meaning that the device can see a circular + * area in front and behind the phone. Other primers can limit the view to forward-only if + * necessary. + */ +public class FovPrimer implements IPrimer { + private final double mCosFov; + + /** + * Creates a new instance of the FovPrimer class. + * @param fov The field-of-view to impose on hardware coordinates. + */ + public FovPrimer(float fov) { + if (fov > F_PI) { + fov = F_PI; + } + this.mCosFov = cos(fov); + } + + /** + * Applies corrections to a raw position. + * @param input The original UWB reading. + * @param prediction A prediction of where the signal probably came from. + * @param poseSource A pose source that may indicate phone orientation. + * @return A replacement value for the UWB input that has been corrected for the situation. + */ + @Override + public SphericalVector.Sparse prime( + @NonNull SphericalVector.Sparse input, + @Nullable SphericalVector prediction, + @Nullable IPoseSource poseSource) { + if (prediction == null) { + return input; + } + + float azimuth = input.hasAzimuth ? input.vector.azimuth : prediction.azimuth; + float elevation = input.hasElevation ? input.vector.elevation : prediction.elevation; + + // Compute the absolute cartesian Z-value of the az/el vector, ignoring distance, + // as an indicator of the position's relation to the FOV. + double zValue = abs(cos(elevation) * cos(azimuth)); + + // Faster equivalent to acos(zValue) < mFov + if (zValue < mCosFov) { + return SphericalVector.fromRadians( + prediction.azimuth, + prediction.elevation, + input.vector.distance + ).toSparse(true, true, input.hasDistance); + } + + return input; + } +} diff --git a/service/java/com/android/server/uwb/correction/primers/IPrimer.java b/service/java/com/android/server/uwb/correction/primers/IPrimer.java new file mode 100644 index 00000000..516b79b0 --- /dev/null +++ b/service/java/com/android/server/uwb/correction/primers/IPrimer.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.primers; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.server.uwb.correction.math.SphericalVector; +import com.android.server.uwb.correction.pose.IPoseSource; + +/** + * Given known data about a UWB reading, applies corrections that correct for nonlinearities, + * missing data or other hardware limitations. + */ +public interface IPrimer { + /** + * Applies corrections to a raw position. + * + * @param input The original UWB reading. + * @param prediction A prediction of where the signal probably came from. + * @param poseSource A pose source that may indicate phone orientation. + * @return A replacement value for the UWB input that has been corrected for the situation. + */ + SphericalVector.Sparse prime( + @NonNull SphericalVector.Sparse input, + @Nullable SphericalVector prediction, + @Nullable IPoseSource poseSource + ); +} diff --git a/service/java/com/android/server/uwb/data/UwbUciConstants.java b/service/java/com/android/server/uwb/data/UwbUciConstants.java index f43b9e06..a6cf5b31 100644 --- a/service/java/com/android/server/uwb/data/UwbUciConstants.java +++ b/service/java/com/android/server/uwb/data/UwbUciConstants.java @@ -15,10 +15,10 @@ */ 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.ccc.CccParams; import com.google.uwb.support.fira.FiraParams; public class UwbUciConstants { @@ -35,10 +35,12 @@ public class UwbUciConstants { /** * 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; + public static final byte SESSION_TYPE_RANGING = FiraParams.SESSION_TYPE_RANGING; + public static final byte SESSION_TYPE_DATA_TRANSFER = + FiraParams.SESSION_TYPE_RANGING_AND_IN_BAND_DATA; + public static final byte SESSION_TYPE_CCC = (byte) CccParams.SESSION_TYPE_CCC; + public static final byte SESSION_TYPE_DEVICE_TEST_MODE = + (byte) FiraParams.SESSION_TYPE_DEVICE_TEST_MODE; /** * Table 14: Control Messages to De-Initialize UWB session - SESSION_STATUS_NTF diff --git a/service/java/com/android/server/uwb/jni/NativeUwbManager.java b/service/java/com/android/server/uwb/jni/NativeUwbManager.java index 4643b72e..abaaf0c9 100644 --- a/service/java/com/android/server/uwb/jni/NativeUwbManager.java +++ b/service/java/com/android/server/uwb/jni/NativeUwbManager.java @@ -403,9 +403,10 @@ public class NativeUwbManager { * Send payload data to a remote device in a UWB ranging session. */ public byte sendData( - int sessionId, byte[] address, byte destEndPoint, int sequenceNum, byte[] appData) { + int sessionId, byte[] address, byte destEndPoint, byte sequenceNum, byte[] appData, + String chipId) { synchronized (mNativeLock) { - return nativeSendData(sessionId, address, destEndPoint, sequenceNum, appData); + return nativeSendData(sessionId, address, destEndPoint, sequenceNum, appData, chipId); } } @@ -427,9 +428,8 @@ public class NativeUwbManager { } } - // TODO(b/259487023): no native implementation private native byte nativeSendData(int sessionId, byte[] address, byte destEndPoint, - int sequenceNum, byte[] appData); + byte sequenceNum, byte[] appData, String chipId); private native long nativeDispatcherNew(Object[] chipIds); diff --git a/service/java/com/android/server/uwb/params/FiraEncoder.java b/service/java/com/android/server/uwb/params/FiraEncoder.java index 770b97c7..f45dc635 100644 --- a/service/java/com/android/server/uwb/params/FiraEncoder.java +++ b/service/java/com/android/server/uwb/params/FiraEncoder.java @@ -47,7 +47,7 @@ public class FiraEncoder extends TlvEncoder { return null; } - private static boolean hasAoaBoundInRangeDataNfConfig(int rangeDataNtfConfig) { + private static boolean hasAoaBoundInRangeDataNtfConfig(int rangeDataNtfConfig) { return rangeDataNtfConfig == RANGE_DATA_NTF_CONFIG_ENABLE_AOA_LEVEL_TRIG || rangeDataNtfConfig == RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY_AOA_LEVEL_TRIG || rangeDataNtfConfig == RANGE_DATA_NTF_CONFIG_ENABLE_AOA_EDGE_TRIG @@ -103,8 +103,8 @@ public class FiraEncoder extends TlvEncoder { .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.VENDOR_ID, params.getVendorId() != null + ? TlvUtil.getReverseBytes(params.getVendorId()) : null) .putByteArray(ConfigParam.STATIC_STS_IV, params.getStaticStsIV()) .putByte(ConfigParam.NUMBER_OF_STS_SEGMENTS, (byte) params.getStsSegmentCount()) @@ -148,23 +148,23 @@ public class FiraEncoder extends TlvEncoder { .putByte(ConfigParam.NUM_AOA_ELEVATION_MEASUREMENTS, (byte) params.getNumOfMsrmtFocusOnAoaElevation()); } - if (hasAoaBoundInRangeDataNfConfig(params.getRangeDataNtfConfig())) { - tlvBufferBuilder.putByteArray(ConfigParam.RANGE_DATA_NTF_AOA_BOUND, new byte[]{ + if (hasAoaBoundInRangeDataNtfConfig(params.getRangeDataNtfConfig())) { + tlvBufferBuilder.putShortArray(ConfigParam.RANGE_DATA_NTF_AOA_BOUND, new short[]{ // TODO (b/235355249): Verify this conversion. This is using AOA value // in UwbTwoWayMeasurement to external RangingMeasurement conversion as // reference. - (byte) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( + (short) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( UwbUtil.radianTodegree( - params.getRangeDataNtfAoaAzimuthLower()), 9, 7), 8), - (byte) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( + params.getRangeDataNtfAoaAzimuthLower()), 9, 7), 16), + (short) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( UwbUtil.radianTodegree( - params.getRangeDataNtfAoaAzimuthUpper()), 9, 7), 8), - (byte) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( + params.getRangeDataNtfAoaAzimuthUpper()), 9, 7), 16), + (short) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( UwbUtil.radianTodegree( - params.getRangeDataNtfAoaElevationLower()), 9, 7), 8), - (byte) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( + params.getRangeDataNtfAoaElevationLower()), 9, 7), 16), + (short) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( UwbUtil.radianTodegree( - params.getRangeDataNtfAoaElevationLower()), 9, 7), 8), + params.getRangeDataNtfAoaElevationUpper()), 9, 7), 16), }); } if (params.isRssiReportingEnabled()) { @@ -180,9 +180,9 @@ public class FiraEncoder extends TlvEncoder { tlvBufferBuilder.putByteArray(ConfigParam.CAP_SIZE_RANGE, params.getCapSize()); } if (params.getDeviceRole() == FiraParams.RANGING_DEVICE_UT_TAG) { - tlvBufferBuilder.putLong(ConfigParam.UL_TDOA_TX_INTERVAL, + tlvBufferBuilder.putInt(ConfigParam.UL_TDOA_TX_INTERVAL, params.getUlTdoaTxIntervalMs()); - tlvBufferBuilder.putLong(ConfigParam.UL_TDOA_RANDOM_WINDOW, + tlvBufferBuilder.putInt(ConfigParam.UL_TDOA_RANDOM_WINDOW, params.getUlTdoaRandomWindowMs()); tlvBufferBuilder.putByteArray(ConfigParam.UL_TDOA_DEVICE_ID, getUlTdoaDeviceId( params.getUlTdoaDeviceIdType(), params.getUlTdoaDeviceId())); @@ -191,7 +191,6 @@ public class FiraEncoder extends TlvEncoder { } return tlvBufferBuilder.build(); } - private byte[] getUlTdoaDeviceId(int ulTdoaDeviceIdType, byte[] ulTdoaDeviceId) { if (ulTdoaDeviceIdType == FiraParams.UL_TDOA_DEVICE_ID_NONE) { // Device ID not included @@ -235,7 +234,7 @@ public class FiraEncoder extends TlvEncoder { (short) rangeDataProximityFar.intValue()); } - if (rangeDataNtfConfig != null && hasAoaBoundInRangeDataNfConfig(rangeDataNtfConfig)) { + if (rangeDataNtfConfig != null && hasAoaBoundInRangeDataNtfConfig(rangeDataNtfConfig)) { if ((rangeDataAoaAzimuthLower != null && rangeDataAoaAzimuthUpper != null) || (rangeDataAoaElevationLower != null && rangeDataAoaElevationUpper != null)) { rangeDataAoaAzimuthLower = rangeDataAoaAzimuthLower != null @@ -250,22 +249,22 @@ public class FiraEncoder extends TlvEncoder { rangeDataAoaElevationUpper = rangeDataAoaElevationUpper != null ? rangeDataAoaElevationUpper : FiraParams.RANGE_DATA_NTF_AOA_ELEVATION_UPPER_DEFAULT; - tlvBuilder.putByteArray(ConfigParam.RANGE_DATA_NTF_AOA_BOUND, new byte[]{ + tlvBuilder.putShortArray(ConfigParam.RANGE_DATA_NTF_AOA_BOUND, new short[]{ // TODO (b/235355249): Verify this conversion. This is using AOA value // in UwbTwoWayMeasurement to external RangingMeasurement conversion as // reference. - (byte) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( + (short) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( UwbUtil.radianTodegree( - rangeDataAoaAzimuthLower.floatValue()), 9, 7), 8), - (byte) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( + rangeDataAoaAzimuthLower.floatValue()), 9, 7), 16), + (short) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( UwbUtil.radianTodegree( - rangeDataAoaAzimuthUpper.floatValue()), 9, 7), 8), - (byte) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( + rangeDataAoaAzimuthUpper.floatValue()), 9, 7), 16), + (short) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( UwbUtil.radianTodegree( - rangeDataAoaElevationLower.floatValue()), 9, 7), 8), - (byte) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( + rangeDataAoaElevationLower.floatValue()), 9, 7), 16), + (short) UwbUtil.twos_compliment(UwbUtil.convertFloatToQFormat( UwbUtil.radianTodegree( - rangeDataAoaElevationUpper.floatValue()), 9, 7), 8), + rangeDataAoaElevationUpper.floatValue()), 9, 7), 16), }); } } diff --git a/service/java/com/android/server/uwb/params/TlvBuffer.java b/service/java/com/android/server/uwb/params/TlvBuffer.java index 167c7f71..3d7ff339 100644 --- a/service/java/com/android/server/uwb/params/TlvBuffer.java +++ b/service/java/com/android/server/uwb/params/TlvBuffer.java @@ -81,6 +81,20 @@ public class TlvBuffer { return this; } + public TlvBuffer.Builder putShortArray(int tagType, short[] sArray) { + if (sArray == null) return this; + return putShortArray(tagType, sArray.length, sArray); + } + + public TlvBuffer.Builder putShortArray(int tagType, int length, short[] sArray) { + addHeader(tagType, length * Short.BYTES); + for (int i = 0; i < length; i++) { + this.mBuffer.put(TlvUtil.getLeBytes(sArray[i])); + } + this.mNoOfParams++; + return this; + } + public TlvBuffer.Builder putInt(int tagType, int data) { addHeader(tagType, Integer.BYTES); this.mBuffer.put(TlvUtil.getLeBytes(data)); diff --git a/service/java/com/android/server/uwb/util/LruList.java b/service/java/com/android/server/uwb/util/LruList.java new file mode 100644 index 00000000..1b5f483b --- /dev/null +++ b/service/java/com/android/server/uwb/util/LruList.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.uwb.util; + +import android.annotation.NonNull; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** + * Utility class for keeping a relatively small List of values, sorted by most recently added + * first. + * + * Copied from {@code packages/modules/Wifi/service/java/com/android/server/wifi/util/LruList.java} + * @param <E> + */ +public class LruList<E> { + private int mSize; + private LinkedList<E> mLinkedList; + + /** + * Creates a new LruList capped by maxSize. + * @param maxSize max allowed size of the LruList + */ + public LruList(int maxSize) { + mSize = maxSize; + mLinkedList = new LinkedList<E>(); + } + + /** + * Add an entry. If the entry already exists then it will be moved to the front. Otherwise, + * a new entry will be added. + * If this operation makes the LruList exceed the max allowed size, then the least recently + * added entry will be removed. + * @param entry + */ + public void add(@NonNull E entry) { + if (entry == null) { + return; + } + int index = mLinkedList.indexOf(entry); + if (index >= 0) { + mLinkedList.remove(index); + } + mLinkedList.addFirst(entry); + while (mLinkedList.size() > mSize) { + mLinkedList.removeLast(); + } + } + + /** + * Remove an entry from list. + */ + public void remove(@NonNull E entry) { + if (entry == null) { + return; + } + int index = mLinkedList.indexOf(entry); + if (index < 0) { + return; + } + mLinkedList.remove(index); + } + + /** + * Returns the list of entries sorted by most recently added entries first. + * @return + */ + public @NonNull List<E> getEntries() { + return new ArrayList<E>(mLinkedList); + } + + /** + * Gets the number of entries in this LruList. + */ + public int size() { + return mLinkedList.size(); + } + + /** + * Get the index in the list of the input entry. + * If not in the list will return -1. + * If in the list, smaller index is more recently added. + */ + public int indexOf(E entry) { + return mLinkedList.indexOf(entry); + } +} diff --git a/service/java/com/android/server/uwb/util/UwbUtil.java b/service/java/com/android/server/uwb/util/UwbUtil.java index d1f94cb1..30645678 100755 --- a/service/java/com/android/server/uwb/util/UwbUtil.java +++ b/service/java/com/android/server/uwb/util/UwbUtil.java @@ -40,6 +40,7 @@ public final class UwbUtil { return sb.toString(); } + /** Convert the given int to a 4-byte hex-string */ public static String toHexString(int var) { byte[] byteArray = new byte[4]; byteArray[0] = (byte) (var & 0xff); @@ -54,6 +55,26 @@ public final class UwbUtil { return sb.toString(); } + /** Convert the given long to an 8-byte hex-string */ + public static String toHexString(long var) { + byte[] byteArray = new byte[8]; + byteArray[0] = (byte) (var & 0xff); + byteArray[1] = (byte) ((var >> 8) & 0xff); + byteArray[2] = (byte) ((var >> 16) & 0xff); + byteArray[3] = (byte) ((var >> 24) & 0xff); + byteArray[4] = (byte) ((var >> 32) & 0xff); + byteArray[5] = (byte) ((var >> 40) & 0xff); + byteArray[6] = (byte) ((var >> 48) & 0xff); + byteArray[7] = (byte) ((var >> 56) & 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]; 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 index f0323b16..cdc76d37 100644 --- a/service/support_lib/src/com/google/uwb/support/ccc/CccOpenRangingParams.java +++ b/service/support_lib/src/com/google/uwb/support/ccc/CccOpenRangingParams.java @@ -42,6 +42,7 @@ public class CccOpenRangingParams extends CccParams { 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_SESSION_TYPE = "session_type"; 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"; @@ -55,6 +56,7 @@ public class CccOpenRangingParams extends CccParams { @UwbConfig private final int mUwbConfig; private final CccPulseShapeCombo mPulseShapeCombo; private final int mSessionId; + @SessionType private final int mSessionType; private final int mRanMultiplier; @Channel private final int mChannel; private final int mNumChapsPerSlot; @@ -69,6 +71,7 @@ public class CccOpenRangingParams extends CccParams { @UwbConfig int uwbConfig, CccPulseShapeCombo pulseShapeCombo, int sessionId, + @SessionType int sessionType, int ranMultiplier, @Channel int channel, int numChapsPerSlot, @@ -81,6 +84,7 @@ public class CccOpenRangingParams extends CccParams { mUwbConfig = uwbConfig; mPulseShapeCombo = pulseShapeCombo; mSessionId = sessionId; + mSessionType = sessionType; mRanMultiplier = ranMultiplier; mChannel = channel; mNumChapsPerSlot = numChapsPerSlot; @@ -103,6 +107,7 @@ public class CccOpenRangingParams extends CccParams { bundle.putInt(KEY_UWB_CONFIG, mUwbConfig); bundle.putString(KEY_PULSE_SHAPE_COMBO, mPulseShapeCombo.toString()); bundle.putInt(KEY_SESSION_ID, mSessionId); + bundle.putInt(KEY_SESSION_TYPE, mSessionType); bundle.putInt(KEY_RAN_MULTIPLIER, mRanMultiplier); bundle.putInt(KEY_CHANNEL, mChannel); bundle.putInt(KEY_NUM_CHAPS_PER_SLOT, mNumChapsPerSlot); @@ -166,6 +171,11 @@ public class CccOpenRangingParams extends CccParams { return mSessionId; } + @SessionType + public int getSessionType() { + return mSessionType; + } + @IntRange(from = 0, to = 255) public int getRanMultiplier() { return mRanMultiplier; @@ -209,6 +219,7 @@ public class CccOpenRangingParams extends CccParams { @UwbConfig private RequiredParam<Integer> mUwbConfig = new RequiredParam<>(); private RequiredParam<CccPulseShapeCombo> mPulseShapeCombo = new RequiredParam<>(); private RequiredParam<Integer> mSessionId = new RequiredParam<>(); + @SessionType private int mSessionType = CccParams.SESSION_TYPE_CCC; private RequiredParam<Integer> mRanMultiplier = new RequiredParam<>(); @Channel private RequiredParam<Integer> mChannel = new RequiredParam<>(); @ChapsPerSlot private RequiredParam<Integer> mNumChapsPerSlot = new RequiredParam<>(); @@ -228,6 +239,7 @@ public class CccOpenRangingParams extends CccParams { mUwbConfig.set(builder.mUwbConfig.get()); mPulseShapeCombo.set(builder.mPulseShapeCombo.get()); mSessionId.set(builder.mSessionId.get()); + mSessionType = builder.mSessionType; mRanMultiplier.set(builder.mRanMultiplier.get()); mChannel.set(builder.mChannel.get()); mNumChapsPerSlot.set(builder.mNumChapsPerSlot.get()); @@ -243,6 +255,7 @@ public class CccOpenRangingParams extends CccParams { mUwbConfig.set(params.mUwbConfig); mPulseShapeCombo.set(params.mPulseShapeCombo); mSessionId.set(params.mSessionId); + mSessionType = params.mSessionType; mRanMultiplier.set(params.mRanMultiplier); mChannel.set(params.mChannel); mNumChapsPerSlot.set(params.mNumChapsPerSlot); @@ -319,6 +332,7 @@ public class CccOpenRangingParams extends CccParams { mUwbConfig.get(), mPulseShapeCombo.get(), mSessionId.get(), + mSessionType, mRanMultiplier.get(), mChannel.get(), mNumChapsPerSlot.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 index ae718787..b241b235 100644 --- a/service/support_lib/src/com/google/uwb/support/ccc/CccParams.java +++ b/service/support_lib/src/com/google/uwb/support/ccc/CccParams.java @@ -200,4 +200,12 @@ public abstract class CccParams extends Params { 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; + + /** Session Type */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + value = {SESSION_TYPE_CCC}) + public @interface SessionType {} + + public static final int SESSION_TYPE_CCC = 160; } 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 index dd8d1322..ff1df5de 100644 --- a/service/support_lib/src/com/google/uwb/support/fira/FiraOpenSessionParams.java +++ b/service/support_lib/src/com/google/uwb/support/fira/FiraOpenSessionParams.java @@ -45,6 +45,7 @@ public class FiraOpenSessionParams extends FiraParams { private final FiraProtocolVersion mProtocolVersion; private final int mSessionId; + @SessionType private final int mSessionType; @RangingDeviceType private final int mDeviceType; @RangingDeviceRole private final int mDeviceRole; @RangingRoundUsage private final int mRangingRoundUsage; @@ -125,8 +126,8 @@ public class FiraOpenSessionParams extends FiraParams { private final int mNumOfMsrmtFocusOnAoaAzimuth; private final int mNumOfMsrmtFocusOnAoaElevation; private final Long mRangingErrorStreakTimeoutMs; - private final long mUlTdoaTxIntervalMs; - private final long mUlTdoaRandomWindowMs; + private final int mUlTdoaTxIntervalMs; + private final int mUlTdoaRandomWindowMs; @UlTdoaDeviceIdType private final int mUlTdoaDeviceIdType; @Nullable private final byte[] mUlTdoaDeviceId; @UlTdoaTxTimestampType private final int mUlTdoaTxTimestampType; @@ -136,6 +137,7 @@ public class FiraOpenSessionParams extends FiraParams { private static final String KEY_PROTOCOL_VERSION = "protocol_version"; private static final String KEY_SESSION_ID = "session_id"; + private static final String KEY_SESSION_TYPE = "session_type"; 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"; @@ -219,6 +221,7 @@ public class FiraOpenSessionParams extends FiraParams { private FiraOpenSessionParams( FiraProtocolVersion protocolVersion, int sessionId, + @SessionType int sessionType, @RangingDeviceType int deviceType, @RangingDeviceRole int deviceRole, @RangingRoundUsage int rangingRoundUsage, @@ -279,13 +282,14 @@ public class FiraOpenSessionParams extends FiraParams { int numOfMsrmtFocusOnAoaAzimuth, int numOfMsrmtFocusOnAoaElevation, Long rangingErrorStreakTimeoutMs, - long ulTdoaTxIntervalMs, - long ulTdoaRandomWindowMs, + int ulTdoaTxIntervalMs, + int ulTdoaRandomWindowMs, int ulTdoaDeviceIdType, @Nullable byte[] ulTdoaDeviceId, int ulTdoaTxTimestampType) { mProtocolVersion = protocolVersion; mSessionId = sessionId; + mSessionType = sessionType; mDeviceType = deviceType; mDeviceRole = deviceRole; mRangingRoundUsage = rangingRoundUsage; @@ -362,6 +366,11 @@ public class FiraOpenSessionParams extends FiraParams { return mSessionId; } + @SessionType + public int getSessionType() { + return mSessionType; + } + @RangingDeviceType public int getDeviceType() { return mDeviceType; @@ -629,11 +638,11 @@ public class FiraOpenSessionParams extends FiraParams { return mRangingErrorStreakTimeoutMs; } - public long getUlTdoaTxIntervalMs() { + public int getUlTdoaTxIntervalMs() { return mUlTdoaTxIntervalMs; } - public long getUlTdoaRandomWindowMs() { + public int getUlTdoaRandomWindowMs() { return mUlTdoaRandomWindowMs; } @@ -680,6 +689,7 @@ public class FiraOpenSessionParams extends FiraParams { PersistableBundle bundle = super.toBundle(); bundle.putString(KEY_PROTOCOL_VERSION, mProtocolVersion.toString()); bundle.putInt(KEY_SESSION_ID, mSessionId); + bundle.putInt(KEY_SESSION_TYPE, mSessionType); bundle.putInt(KEY_DEVICE_TYPE, mDeviceType); bundle.putInt(KEY_DEVICE_ROLE, mDeviceRole); bundle.putInt(KEY_RANGING_ROUND_USAGE, mRangingRoundUsage); @@ -760,8 +770,8 @@ public class FiraOpenSessionParams extends FiraParams { bundle.putInt(KEY_NUM_OF_MSRMT_FOCUS_ON_AOA_AZIMUTH, mNumOfMsrmtFocusOnAoaAzimuth); bundle.putInt(KEY_NUM_OF_MSRMT_FOCUS_ON_AOA_ELEVATION, mNumOfMsrmtFocusOnAoaElevation); bundle.putLong(RANGING_ERROR_STREAK_TIMEOUT_MS, mRangingErrorStreakTimeoutMs); - bundle.putLong(UL_TDOA_TX_INTERVAL, mUlTdoaTxIntervalMs); - bundle.putLong(UL_TDOA_RANDOM_WINDOW, mUlTdoaRandomWindowMs); + bundle.putInt(UL_TDOA_TX_INTERVAL, mUlTdoaTxIntervalMs); + bundle.putInt(UL_TDOA_RANDOM_WINDOW, mUlTdoaRandomWindowMs); bundle.putInt(UL_TDOA_DEVICE_ID_TYPE, mUlTdoaDeviceIdType); bundle.putIntArray(UL_TDOA_DEVICE_ID, byteArrayToIntArray(mUlTdoaDeviceId)); bundle.putInt(UL_TDOA_TX_TIMESTAMP_TYPE, mUlTdoaTxTimestampType); @@ -802,6 +812,7 @@ public class FiraOpenSessionParams extends FiraParams { FiraProtocolVersion.fromString( requireNonNull(bundle.getString(KEY_PROTOCOL_VERSION)))) .setSessionId(bundle.getInt(KEY_SESSION_ID)) + .setSessionType(bundle.getInt(KEY_SESSION_TYPE, FiraParams.SESSION_TYPE_RANGING)) .setDeviceType(bundle.getInt(KEY_DEVICE_TYPE)) .setDeviceRole(bundle.getInt(KEY_DEVICE_ROLE)) .setRangingRoundUsage(bundle.getInt(KEY_RANGING_ROUND_USAGE)) @@ -810,8 +821,7 @@ public class FiraOpenSessionParams extends FiraParams { .setDestAddressList(destAddressList) // Changed from int to long. Look for int value, if long value not found to // maintain backwards compatibility. - .setInitiationTimeMs(bundle.getLong( - KEY_INITIATION_TIME_MS, bundle.getInt(KEY_INITIATION_TIME_MS))) + .setInitiationTimeMs(bundle.getLong(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)) @@ -881,8 +891,8 @@ public class FiraOpenSessionParams extends FiraParams { bundle.getInt(KEY_NUM_OF_MSRMT_FOCUS_ON_AOA_ELEVATION)) .setRangingErrorStreakTimeoutMs(bundle .getLong(RANGING_ERROR_STREAK_TIMEOUT_MS, 30_000L)) - .setUlTdoaTxIntervalMs(bundle.getLong(UL_TDOA_TX_INTERVAL)) - .setUlTdoaRandomWindowMs(bundle.getLong(UL_TDOA_RANDOM_WINDOW)) + .setUlTdoaTxIntervalMs(bundle.getInt(UL_TDOA_TX_INTERVAL)) + .setUlTdoaRandomWindowMs(bundle.getInt(UL_TDOA_RANDOM_WINDOW)) .setUlTdoaDeviceIdType(bundle.getInt(UL_TDOA_DEVICE_ID_TYPE)) .setUlTdoaDeviceId(intArrayToByteArray(bundle.getIntArray(UL_TDOA_DEVICE_ID))) .setUlTdoaTxTimestampType(bundle.getInt(UL_TDOA_TX_TIMESTAMP_TYPE)) @@ -898,6 +908,8 @@ public class FiraOpenSessionParams extends FiraParams { private final RequiredParam<FiraProtocolVersion> mProtocolVersion = new RequiredParam<>(); private final RequiredParam<Integer> mSessionId = new RequiredParam<>(); + @SessionType + private int mSessionType = FiraParams.SESSION_TYPE_RANGING; private final RequiredParam<Integer> mDeviceType = new RequiredParam<>(); private final RequiredParam<Integer> mDeviceRole = new RequiredParam<>(); @@ -1045,10 +1057,10 @@ public class FiraOpenSessionParams extends FiraParams { /** UCI spec default: +180 (No upper-bound filtering) */ private double mRangeDataNtfAoaAzimuthUpper = RANGE_DATA_NTF_AOA_AZIMUTH_UPPER_DEFAULT; - /** UCI spec default: -180 (No low-bound filtering) */ + /** UCI spec default: -90 (No low-bound filtering) */ private double mRangeDataNtfAoaElevationLower = RANGE_DATA_NTF_AOA_ELEVATION_LOWER_DEFAULT; - /** UCI spec default: +180 (No upper-bound filtering) */ + /** UCI spec default: +90 (No upper-bound filtering) */ private double mRangeDataNtfAoaElevationUpper = RANGE_DATA_NTF_AOA_ELEVATION_UPPER_DEFAULT; /** UCI spec default: RESULT_REPORT_CONFIG bit 0 is 1 */ @@ -1075,10 +1087,10 @@ public class FiraOpenSessionParams extends FiraParams { private long mRangingErrorStreakTimeoutMs = 30_000L; /** Ul-TDoA Tx Interval in Milliseconds */ - private long mUlTdoaTxIntervalMs = 2_000L; + private int mUlTdoaTxIntervalMs = 2000; /** Ul-TDoA Random Window in Milliseconds */ - private long mUlTdoaRandomWindowMs = 0; + private int mUlTdoaRandomWindowMs = 0; /** Ul-TDoA Device ID type */ @UlTdoaDeviceIdType private int mUlTdoaDeviceIdType = UL_TDOA_DEVICE_ID_NONE; @@ -1094,6 +1106,7 @@ public class FiraOpenSessionParams extends FiraParams { public Builder(@NonNull Builder builder) { mProtocolVersion.set(builder.mProtocolVersion.get()); mSessionId.set(builder.mSessionId.get()); + mSessionType = builder.mSessionType; mDeviceType.set(builder.mDeviceType.get()); mDeviceRole.set(builder.mDeviceRole.get()); mRangingRoundUsage = builder.mRangingRoundUsage; @@ -1166,6 +1179,7 @@ public class FiraOpenSessionParams extends FiraParams { public Builder(@NonNull FiraOpenSessionParams params) { mProtocolVersion.set(params.mProtocolVersion); mSessionId.set(params.mSessionId); + mSessionType = params.mSessionType; mDeviceType.set(params.mDeviceType); mDeviceRole.set(params.mDeviceRole); mRangingRoundUsage = params.mRangingRoundUsage; @@ -1245,6 +1259,11 @@ public class FiraOpenSessionParams extends FiraParams { return this; } + public FiraOpenSessionParams.Builder setSessionType(@SessionType int sessionType) { + mSessionType = sessionType; + return this; + } + public FiraOpenSessionParams.Builder setDeviceType(@RangingDeviceType int deviceType) { mDeviceType.set(deviceType); return this; @@ -1577,13 +1596,13 @@ public class FiraOpenSessionParams extends FiraParams { } public FiraOpenSessionParams.Builder setUlTdoaTxIntervalMs( - long ulTdoaTxIntervalMs) { + int ulTdoaTxIntervalMs) { mUlTdoaTxIntervalMs = ulTdoaTxIntervalMs; return this; } public FiraOpenSessionParams.Builder setUlTdoaRandomWindowMs( - long ulTdoaRandomWindowMs) { + int ulTdoaRandomWindowMs) { mUlTdoaRandomWindowMs = ulTdoaRandomWindowMs; return this; } @@ -1754,6 +1773,7 @@ public class FiraOpenSessionParams extends FiraParams { return new FiraOpenSessionParams( mProtocolVersion.get(), mSessionId.get(), + mSessionType, mDeviceType.get(), mDeviceRole.get(), mRangingRoundUsage, 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 index 2ce48bf0..615fa712 100644 --- a/service/support_lib/src/com/google/uwb/support/fira/FiraParams.java +++ b/service/support_lib/src/com/google/uwb/support/fira/FiraParams.java @@ -673,8 +673,8 @@ public abstract class FiraParams extends Params { public static final int RANGE_DATA_NTF_PROXIMITY_FAR_DEFAULT = 20000; public static final double RANGE_DATA_NTF_AOA_AZIMUTH_LOWER_DEFAULT = -Math.PI; public static final double RANGE_DATA_NTF_AOA_AZIMUTH_UPPER_DEFAULT = Math.PI; - public static final double RANGE_DATA_NTF_AOA_ELEVATION_LOWER_DEFAULT = -Math.PI; - public static final double RANGE_DATA_NTF_AOA_ELEVATION_UPPER_DEFAULT = Math.PI; + public static final double RANGE_DATA_NTF_AOA_ELEVATION_LOWER_DEFAULT = -Math.PI / 2; + public static final double RANGE_DATA_NTF_AOA_ELEVATION_UPPER_DEFAULT = Math.PI / 2; public enum AoaCapabilityFlag implements FlagEnum { HAS_AZIMUTH_SUPPORT(1), @@ -1002,6 +1002,29 @@ public abstract class FiraParams extends Params { public static final int KEY_LENGTH_256_BITS_NOT_SUPPORTED = 0; public static final int KEY_LENGTH_256_BITS_SUPPORTED = 1; + /** + * Session Type (for SESSION_INIT_CMD) + */ + @IntDef( + value = { + SESSION_TYPE_RANGING, + SESSION_TYPE_RANGING_AND_IN_BAND_DATA, + SESSION_TYPE_DATA_TRANSFER, + SESSION_TYPE_RANGING_ONLY_PHASE, + SESSION_TYPE_IN_BAND_DATA_PHASE, + SESSION_TYPE_RANGING_WITH_DATA_PHASE, + SESSION_TYPE_DEVICE_TEST_MODE, + }) + public @interface SessionType{} + + public static final int SESSION_TYPE_RANGING = 0; + public static final int SESSION_TYPE_RANGING_AND_IN_BAND_DATA = 1; + public static final int SESSION_TYPE_DATA_TRANSFER = 2; + public static final int SESSION_TYPE_RANGING_ONLY_PHASE = 3; + public static final int SESSION_TYPE_IN_BAND_DATA_PHASE = 4; + public static final int SESSION_TYPE_RANGING_WITH_DATA_PHASE = 5; + public static final int SESSION_TYPE_DEVICE_TEST_MODE = 0xD0; + // Helper functions protected static UwbAddress longToUwbAddress(long value, int length) { ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); diff --git a/service/support_lib/test/CccTests.java b/service/support_lib/test/CccTests.java index e9a0fde6..d5431cf0 100644 --- a/service/support_lib/test/CccTests.java +++ b/service/support_lib/test/CccTests.java @@ -83,6 +83,7 @@ public class CccTests { assertEquals( params.getPulseShapeCombo().getResponderTx(), pulseShapeCombo.getResponderTx()); assertEquals(params.getSessionId(), sessionId); + assertEquals(params.getSessionType(), CccParams.SESSION_TYPE_CCC); assertEquals(params.getRanMultiplier(), ranMultiplier); assertEquals(params.getChannel(), channel); assertEquals(params.getNumChapsPerSlot(), chapsPerSlot); diff --git a/service/support_lib/test/FiraTests.java b/service/support_lib/test/FiraTests.java index 48ec79ef..833b2c4f 100644 --- a/service/support_lib/test/FiraTests.java +++ b/service/support_lib/test/FiraTests.java @@ -35,6 +35,7 @@ import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_TYPE_CONTROL 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 com.google.uwb.support.fira.FiraParams.RFRAME_CONFIG_SP1; +import static com.google.uwb.support.fira.FiraParams.SESSION_TYPE_RANGING; 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; @@ -79,6 +80,7 @@ public class FiraTests { public void testOpenSessionParams() { FiraProtocolVersion protocolVersion = FiraParams.PROTOCOL_VERSION_1_1; int sessionId = 10; + int sessionType = SESSION_TYPE_RANGING; int deviceType = RANGING_DEVICE_TYPE_CONTROLEE; int deviceRole = RANGING_DEVICE_ROLE_INITIATOR; int rangingRoundUsage = RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE; @@ -130,7 +132,7 @@ public class FiraTests { double rangeDataNtfAoaAzimuthLower = -0.5; double rangeDataNtfAoaAzimuthUpper = +1.5; double rangeDataNtfAoaElevationLower = -1.5; - double rangeDataNtfAoaElevationUpper = +2.5; + double rangeDataNtfAoaElevationUpper = +1.2; boolean hasTimeOfFlightReport = true; boolean hasAngleOfArrivalAzimuthReport = true; boolean hasAngleOfArrivalElevationReport = true; @@ -139,8 +141,8 @@ public class FiraTests { int numOfMsrmtFocusOnRange = 1; int numOfMsrmtFocusOnAoaAzimuth = 2; int numOfMsrmtFocusOnAoaElevation = 3; - long ulTdoaTxIntervalMs = 1_000L; - long ulTdoaRandomWindowMS = 100; + int ulTdoaTxIntervalMs = 1_000; + int ulTdoaRandomWindowMS = 100; int ulTdoaDeviceIdType = UL_TDOA_DEVICE_ID_16_BIT; byte[] ulTdoaDeviceId = new byte[] {(byte) 0x0C, (byte) 0x0B}; int ulTdoaTxTimestampType = TX_TIMESTAMP_40_BIT; @@ -149,6 +151,7 @@ public class FiraTests { new FiraOpenSessionParams.Builder() .setProtocolVersion(protocolVersion) .setSessionId(sessionId) + .setSessionType(sessionType) .setDeviceType(deviceType) .setDeviceRole(deviceRole) .setRangingRoundUsage(rangingRoundUsage) @@ -213,6 +216,7 @@ public class FiraTests { assertEquals(params.getProtocolVersion(), protocolVersion); assertEquals(params.getSessionId(), sessionId); + assertEquals(params.getSessionType(), sessionType); assertEquals(params.getDeviceType(), deviceType); assertEquals(params.getDeviceRole(), deviceRole); assertEquals(params.getRangingRoundUsage(), rangingRoundUsage); @@ -442,7 +446,7 @@ public class FiraTests { double rangeDataAoaAzimuthLower = -0.5; double rangeDataAoaAzimuthUpper = +1.5; double rangeDataAoaElevationLower = -1.5; - double rangeDataAoaElevationUpper = +2.5; + double rangeDataAoaElevationUpper = +1.2; int[] subSessionIdList = new int[] {3, 4}; FiraRangingReconfigureParams params = diff --git a/service/tests/src/com/android/server/uwb/UwbConfigurationManagerTest.java b/service/tests/src/com/android/server/uwb/UwbConfigurationManagerTest.java index d6cffe4c..a5b6c049 100644 --- a/service/tests/src/com/android/server/uwb/UwbConfigurationManagerTest.java +++ b/service/tests/src/com/android/server/uwb/UwbConfigurationManagerTest.java @@ -31,6 +31,7 @@ import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_ROLE_INITIAT 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.SESSION_TYPE_RANGING; 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; @@ -143,6 +144,7 @@ public class UwbConfigurationManagerTest { private FiraOpenSessionParams getFiraParams() { FiraProtocolVersion protocolVersion = FiraParams.PROTOCOL_VERSION_1_1; int sessionId = 10; + int sessionType = SESSION_TYPE_RANGING; int deviceType = RANGING_DEVICE_TYPE_CONTROLEE; int deviceRole = RANGING_DEVICE_ROLE_INITIATOR; int rangingRoundUsage = RANGING_ROUND_USAGE_SS_TWR_DEFERRED_MODE; @@ -204,6 +206,7 @@ public class UwbConfigurationManagerTest { new FiraOpenSessionParams.Builder() .setProtocolVersion(protocolVersion) .setSessionId(sessionId) + .setSessionType(sessionType) .setDeviceType(deviceType) .setDeviceRole(deviceRole) .setRangingRoundUsage(rangingRoundUsage) diff --git a/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java b/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java index abc7b34c..5d780fe0 100644 --- a/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java +++ b/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java @@ -31,6 +31,7 @@ import static com.google.uwb.support.fira.FiraParams.RANGE_DATA_NTF_CONFIG_ENABL 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 com.google.uwb.support.fira.FiraParams.SESSION_TYPE_RANGING; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; @@ -105,7 +106,9 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.MockitoSession; +import org.mockito.invocation.InvocationOnMock; import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; import java.util.Arrays; import java.util.List; @@ -136,6 +139,7 @@ public class UwbServiceCoreTest { new FiraOpenSessionParams.Builder() .setProtocolVersion(FiraParams.PROTOCOL_VERSION_1_1) .setSessionId(1) + .setSessionType(SESSION_TYPE_RANGING) .setDeviceType(RANGING_DEVICE_TYPE_CONTROLLER) .setDeviceRole(RANGING_DEVICE_ROLE_RESPONDER) .setDeviceAddress(UwbAddress.fromBytes(new byte[] { 0x4, 0x6})) @@ -172,6 +176,7 @@ public class UwbServiceCoreTest { @Mock private UwbInjector mUwbInjector; @Mock DeviceConfigFacade mDeviceConfigFacade; @Mock private ProfileManager mProfileManager; + @Mock private PowerManager.WakeLock mUwbWakeLock; @Mock private Resources mResources; private TestLooper mTestLooper; @@ -187,7 +192,7 @@ public class UwbServiceCoreTest { mTestLooper = new TestLooper(); PowerManager powerManager = mock(PowerManager.class); when(powerManager.newWakeLock(anyInt(), anyString())) - .thenReturn(mock(PowerManager.WakeLock.class)); + .thenReturn(mUwbWakeLock); when(mContext.getSystemService(PowerManager.class)).thenReturn(powerManager); when(mUwbInjector.getDeviceConfigFacade()).thenReturn(mDeviceConfigFacade); UwbMultichipData uwbMultichipData = setUpMultichipDataForOneChip(); @@ -386,6 +391,41 @@ public class UwbServiceCoreTest { verifyNoMoreInteractions(mNativeUwbManager, mUwbCountryCode, cb); } + // Test the UWB stack enable when the NativeUwbManager.doInitialize() is delayed such that the + // watchdog timer expiry happens. + @Test + public void testEnableWhenInitializeDelayed() throws Exception { + IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class); + when(cb.asBinder()).thenReturn(mock(IBinder.class)); + mUwbServiceCore.registerAdapterStateCallbacks(cb); + verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_DISABLED, + StateChangeReason.SYSTEM_BOOT); + + // Setup doInitialize() to take long time, such that the WatchDog thread times out. + when(mNativeUwbManager.doInitialize()).thenAnswer(new Answer<Boolean>() { + public Boolean answer(InvocationOnMock invocation) throws Throwable { + // Return success but too late, so this result shouldn't matter. + Thread.sleep(UwbServiceCore.WATCHDOG_MS + 1000); + return true; + } + }); + when(mUwbCountryCode.getCountryCode()).thenReturn("US"); + when(mUwbCountryCode.setCountryCode(anyBoolean())).thenReturn(true); + + // Setup the wakelock to be checked twice (once from the watchdog thread after expiry, and + // second time from handleEnable()). + when(mUwbWakeLock.isHeld()).thenReturn(true).thenReturn(false); + + mUwbServiceCore.setEnabled(true); + mTestLooper.dispatchAll(); + + verify(mNativeUwbManager).doInitialize(); + verify(mUwbWakeLock, times(1)).acquire(); + verify(mUwbWakeLock, times(2)).isHeld(); + verify(mUwbWakeLock, times(1)).release(); + verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE, + StateChangeReason.SYSTEM_POLICY); + } @Test public void testDisable() throws Exception { @@ -407,6 +447,47 @@ public class UwbServiceCoreTest { StateChangeReason.SYSTEM_POLICY); } + // Test the UWB stack disable when the NativeUwbManager.doDeinitialize() is delayed such that + // the watchdog timer expiry happens. + @Test + public void testDisableWhenInitializeDelayed() throws Exception { + IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class); + when(cb.asBinder()).thenReturn(mock(IBinder.class)); + mUwbServiceCore.registerAdapterStateCallbacks(cb); + verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_DISABLED, + StateChangeReason.SYSTEM_BOOT); + + // Enable first + enableUwbWithCountryCode(); + verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE, + StateChangeReason.SYSTEM_POLICY); + + clearInvocations(mUwbWakeLock); + + // Setup doDeinitialize() to take long time, such that the WatchDog thread times out. + when(mNativeUwbManager.doDeinitialize()).thenAnswer(new Answer<Boolean>() { + public Boolean answer(InvocationOnMock invocation) throws Throwable { + // Return success but too late, so this result shouldn't matter. + Thread.sleep(UwbServiceCore.WATCHDOG_MS + 1000); + return true; + } + }); + + // Setup the wakelock to be checked twice (once from the watchdog thread after expiry, and + // second time from handleDisable()). + when(mUwbWakeLock.isHeld()).thenReturn(true).thenReturn(false); + + // Disable UWB. + mUwbServiceCore.setEnabled(false); + mTestLooper.dispatchAll(); + + verify(mNativeUwbManager).doDeinitialize(); + verify(mUwbWakeLock, times(1)).acquire(); + verify(mUwbWakeLock, times(2)).isHeld(); + verify(mUwbWakeLock, times(1)).release(); + verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_DISABLED, + StateChangeReason.SYSTEM_POLICY); + } @Test public void testDisableWhenAlreadyDisabled() throws Exception { @@ -547,7 +628,8 @@ public class UwbServiceCoreTest { verify(mUwbSessionManager).initSession( eq(attributionSource), - eq(sessionHandle), eq(params.getSessionId()), eq(FiraParams.PROTOCOL_NAME), + eq(sessionHandle), eq(params.getSessionId()), eq((byte) params.getSessionType()), + eq(FiraParams.PROTOCOL_NAME), argThat(p -> ((FiraOpenSessionParams) p).getSessionId() == params.getSessionId()), eq(cb), eq(TEST_DEFAULT_CHIP_ID)); @@ -566,7 +648,8 @@ public class UwbServiceCoreTest { verify(mUwbSessionManager).initSession( eq(attributionSource), - eq(sessionHandle), eq(params.getSessionId()), eq(CccParams.PROTOCOL_NAME), + eq(sessionHandle), eq(params.getSessionId()), eq((byte) params.getSessionType()), + eq(CccParams.PROTOCOL_NAME), argThat(p -> ((CccOpenRangingParams) p).getSessionId() == params.getSessionId()), eq(cb), eq(TEST_DEFAULT_CHIP_ID)); } @@ -903,6 +986,37 @@ public class UwbServiceCoreTest { } @Test + public void testMultipleDeviceStateCallbacks() throws Exception { + IUwbAdapterStateCallbacks cb1 = mock(IUwbAdapterStateCallbacks.class); + when(cb1.asBinder()).thenReturn(mock(IBinder.class)); + IUwbAdapterStateCallbacks cb2 = mock(IUwbAdapterStateCallbacks.class); + when(cb2.asBinder()).thenReturn(mock(IBinder.class)); + + mUwbServiceCore.registerAdapterStateCallbacks(cb1); + verify(cb1).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_DISABLED, + StateChangeReason.SYSTEM_BOOT); + + mUwbServiceCore.registerAdapterStateCallbacks(cb2); + verify(cb2).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_DISABLED, + StateChangeReason.SYSTEM_BOOT); + + enableUwbWithCountryCode(); + verify(cb1).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE, + StateChangeReason.SYSTEM_POLICY); + verify(cb2).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE, + StateChangeReason.SYSTEM_POLICY); + + when(mUwbCountryCode.getCountryCode()).thenReturn("US"); + mUwbServiceCore.onDeviceStatusNotificationReceived(UwbUciConstants.DEVICE_STATE_ACTIVE, + TEST_DEFAULT_CHIP_ID); + mTestLooper.dispatchAll(); + verify(cb1).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_ENABLED_ACTIVE, + StateChangeReason.SESSION_STARTED); + verify(cb2).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_ENABLED_ACTIVE, + StateChangeReason.SESSION_STARTED); + } + + @Test public void testDeviceStateCallback_invalidChipId() throws Exception { IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class); when(cb.asBinder()).thenReturn(mock(IBinder.class)); diff --git a/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java b/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java index ca230446..f8f10b6d 100644 --- a/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java +++ b/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java @@ -24,14 +24,20 @@ import static com.android.server.uwb.UwbTestUtils.DATA_PAYLOAD; import static com.android.server.uwb.UwbTestUtils.PEER_BAD_MAC_ADDRESS; import static com.android.server.uwb.UwbTestUtils.PEER_EXTENDED_MAC_ADDRESS; import static com.android.server.uwb.UwbTestUtils.PEER_EXTENDED_MAC_ADDRESS_2; +import static com.android.server.uwb.UwbTestUtils.PEER_EXTENDED_MAC_ADDRESS_2_LONG; +import static com.android.server.uwb.UwbTestUtils.PEER_EXTENDED_MAC_ADDRESS_LONG; import static com.android.server.uwb.UwbTestUtils.PEER_EXTENDED_SHORT_MAC_ADDRESS; +import static com.android.server.uwb.UwbTestUtils.PEER_EXTENDED_SHORT_MAC_ADDRESS_LONG; import static com.android.server.uwb.UwbTestUtils.PEER_EXTENDED_UWB_ADDRESS; +import static com.android.server.uwb.UwbTestUtils.PEER_EXTENDED_UWB_ADDRESS_2; import static com.android.server.uwb.UwbTestUtils.PEER_SHORT_MAC_ADDRESS; +import static com.android.server.uwb.UwbTestUtils.PEER_SHORT_MAC_ADDRESS_LONG; import static com.android.server.uwb.UwbTestUtils.PEER_SHORT_UWB_ADDRESS; import static com.android.server.uwb.UwbTestUtils.PERSISTABLE_BUNDLE; import static com.android.server.uwb.UwbTestUtils.RANGING_MEASUREMENT_TYPE_UNDEFINED; import static com.android.server.uwb.UwbTestUtils.TEST_SESSION_ID; import static com.android.server.uwb.UwbTestUtils.TEST_SESSION_ID_2; +import static com.android.server.uwb.UwbTestUtils.TEST_SESSION_TYPE; import static com.android.server.uwb.data.UwbUciConstants.MAC_ADDRESSING_MODE_EXTENDED; import static com.android.server.uwb.data.UwbUciConstants.MAC_ADDRESSING_MODE_SHORT; import static com.android.server.uwb.data.UwbUciConstants.RANGING_DEVICE_ROLE_ADVERTISER; @@ -45,9 +51,9 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.uwb.support.fira.FiraParams.PROTOCOL_NAME; import static com.google.uwb.support.fira.FiraParams.RangeDataNtfConfigCapabilityFlag.HAS_RANGE_DATA_NTF_CONFIG_DISABLE; import static com.google.uwb.support.fira.FiraParams.RangeDataNtfConfigCapabilityFlag.HAS_RANGE_DATA_NTF_CONFIG_ENABLE; +import static com.google.uwb.support.fira.FiraParams.SESSION_TYPE_RANGING; +import static com.google.uwb.support.fira.FiraParams.STATUS_CODE_OK; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyByte; import static org.mockito.ArgumentMatchers.anyInt; @@ -56,6 +62,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; @@ -64,6 +71,7 @@ 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.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -141,7 +149,9 @@ public class UwbSessionManagerTest { private static final UwbAddress UWB_DEST_ADDRESS_3 = UwbAddress.fromBytes(new byte[] {(byte) 0x07, (byte) 0x08 }); private static final int TEST_RANGING_INTERVAL_MS = 200; - private static final int DATA_SEQUENCE_NUM = 1; + private static final byte DATA_SEQUENCE_NUM = 0; + private static final byte DATA_SEQUENCE_NUM_1 = 2; + private static final int SOURCE_END_POINT = 100; private static final int DEST_END_POINT = 200; private static final int HANDLE_ID = 12; @@ -222,26 +232,42 @@ public class UwbSessionManagerTest { @Test public void onDataReceived_extendedMacAddressFormat() { + UwbSession mockUwbSession = mock(UwbSession.class); + when(mockUwbSession.getWaitObj()).thenReturn(mock(WaitObj.class)); + doReturn(mockUwbSession) + .when(mUwbSessionManager).getUwbSession(eq(TEST_SESSION_ID)); + mUwbSessionManager.onDataReceived(TEST_SESSION_ID, UwbUciConstants.STATUS_CODE_OK, DATA_SEQUENCE_NUM, PEER_EXTENDED_MAC_ADDRESS, SOURCE_END_POINT, DEST_END_POINT, DATA_PAYLOAD); - assertNotNull(mUwbSessionManager.getReceivedDataInfo(PEER_EXTENDED_MAC_ADDRESS)); + verify(mockUwbSession).addReceivedDataInfo(isA(UwbSessionManager.ReceivedDataInfo.class)); } @Test public void onDataReceived_unsupportedMacAddressLength() { + UwbSession mockUwbSession = mock(UwbSession.class); + when(mockUwbSession.getWaitObj()).thenReturn(mock(WaitObj.class)); + doReturn(mockUwbSession) + .when(mUwbSessionManager).getUwbSession(eq(TEST_SESSION_ID)); + mUwbSessionManager.onDataReceived(TEST_SESSION_ID, UwbUciConstants.STATUS_CODE_OK, DATA_SEQUENCE_NUM, PEER_BAD_MAC_ADDRESS, SOURCE_END_POINT, DEST_END_POINT, DATA_PAYLOAD); - assertNull(mUwbSessionManager.getReceivedDataInfo(PEER_BAD_MAC_ADDRESS)); + verify(mockUwbSession, never()).addReceivedDataInfo( + isA(UwbSessionManager.ReceivedDataInfo.class)); } @Test public void onDataReceived_shortMacAddressFormat() { + UwbSession mockUwbSession = mock(UwbSession.class); + when(mockUwbSession.getWaitObj()).thenReturn(mock(WaitObj.class)); + doReturn(mockUwbSession) + .when(mUwbSessionManager).getUwbSession(eq(TEST_SESSION_ID)); + mUwbSessionManager.onDataReceived(TEST_SESSION_ID, UwbUciConstants.STATUS_CODE_OK, DATA_SEQUENCE_NUM, PEER_EXTENDED_SHORT_MAC_ADDRESS, SOURCE_END_POINT, DEST_END_POINT, DATA_PAYLOAD); - assertNotNull(mUwbSessionManager.getReceivedDataInfo(PEER_EXTENDED_SHORT_MAC_ADDRESS)); + verify(mockUwbSession).addReceivedDataInfo(isA(UwbSessionManager.ReceivedDataInfo.class)); } @Test @@ -296,6 +322,7 @@ public class UwbSessionManagerTest { mUwbSessionManager.onDataReceived(TEST_SESSION_ID, UwbUciConstants.STATUS_CODE_OK, DATA_SEQUENCE_NUM, PEER_EXTENDED_MAC_ADDRESS, SOURCE_END_POINT, DEST_END_POINT, DATA_PAYLOAD); + verify(mockUwbSession).addReceivedDataInfo(isA(UwbSessionManager.ReceivedDataInfo.class)); // Next call onRangeDataNotificationReceived() to process the RANGE_DATA_NTF. UwbRangingData uwbRangingData = UwbTestUtils.generateRangingData( @@ -305,6 +332,8 @@ public class UwbSessionManagerTest { RANGING_DEVICE_ROLE_OBSERVER, Optional.of(ROUND_USAGE_OWR_AOA_MEASUREMENT)); when(mockUwbSession.getParams()).thenReturn(firaParams); when(mUwbAdvertiseManager.isPointedTarget(PEER_EXTENDED_MAC_ADDRESS)).thenReturn(true); + when(mockUwbSession.getAllReceivedDataInfo(PEER_EXTENDED_MAC_ADDRESS_LONG)) + .thenReturn(List.of(buildReceivedDataInfo(PEER_EXTENDED_MAC_ADDRESS_LONG))); mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData); verify(mUwbSessionNotificationManager) @@ -313,7 +342,7 @@ public class UwbSessionManagerTest { verify(mUwbSessionNotificationManager) .onDataReceived(eq(mockUwbSession), eq(PEER_EXTENDED_UWB_ADDRESS), isA(PersistableBundle.class), eq(DATA_PAYLOAD)); - verify(mUwbAdvertiseManager).removeAdvertiseTarget(PEER_EXTENDED_MAC_ADDRESS); + verify(mUwbAdvertiseManager).removeAdvertiseTarget(PEER_EXTENDED_MAC_ADDRESS_LONG); verify(mUwbMetrics).logRangingResult(anyInt(), eq(uwbRangingData)); } @@ -338,6 +367,7 @@ public class UwbSessionManagerTest { mUwbSessionManager.onDataReceived(TEST_SESSION_ID, UwbUciConstants.STATUS_CODE_OK, DATA_SEQUENCE_NUM, PEER_EXTENDED_SHORT_MAC_ADDRESS, SOURCE_END_POINT, DEST_END_POINT, DATA_PAYLOAD); + verify(mockUwbSession).addReceivedDataInfo(isA(UwbSessionManager.ReceivedDataInfo.class)); // Next call onRangeDataNotificationReceived() to process the RANGE_DATA_NTF. UwbRangingData uwbRangingData = UwbTestUtils.generateRangingData( @@ -347,6 +377,8 @@ public class UwbSessionManagerTest { RANGING_DEVICE_ROLE_OBSERVER, Optional.of(ROUND_USAGE_OWR_AOA_MEASUREMENT)); when(mockUwbSession.getParams()).thenReturn(firaParams); when(mUwbAdvertiseManager.isPointedTarget(PEER_SHORT_MAC_ADDRESS)).thenReturn(true); + when(mockUwbSession.getAllReceivedDataInfo(PEER_EXTENDED_SHORT_MAC_ADDRESS_LONG)) + .thenReturn(List.of(buildReceivedDataInfo(PEER_EXTENDED_SHORT_MAC_ADDRESS_LONG))); mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData); verify(mUwbSessionNotificationManager) @@ -355,10 +387,80 @@ public class UwbSessionManagerTest { verify(mUwbSessionNotificationManager) .onDataReceived(eq(mockUwbSession), eq(PEER_SHORT_UWB_ADDRESS), isA(PersistableBundle.class), eq(DATA_PAYLOAD)); - verify(mUwbAdvertiseManager).removeAdvertiseTarget(PEER_SHORT_MAC_ADDRESS); + verify(mUwbAdvertiseManager).removeAdvertiseTarget(PEER_SHORT_MAC_ADDRESS_LONG); verify(mUwbMetrics).logRangingResult(anyInt(), eq(uwbRangingData)); } + // Test scenario for receiving Application payload data followed by a RANGE_DATA_NTF with an + // OWR Aoa Measurement, from Multiple advertiser devices in a UWB session. + @Test + public void onRangeDataNotificationReceived_owrAoa_success_multipleAdvertisers() { + UwbSession mockUwbSession = mock(UwbSession.class); + when(mockUwbSession.getWaitObj()).thenReturn(mock(WaitObj.class)); + doReturn(mockUwbSession) + .when(mUwbSessionManager).getUwbSession(eq(TEST_SESSION_ID)); + + // First call onDataReceived() to get the application payload data. + mUwbSessionManager.onDataReceived(TEST_SESSION_ID, UwbUciConstants.STATUS_CODE_OK, + DATA_SEQUENCE_NUM, PEER_EXTENDED_MAC_ADDRESS, SOURCE_END_POINT, DEST_END_POINT, + DATA_PAYLOAD); + mUwbSessionManager.onDataReceived(TEST_SESSION_ID, UwbUciConstants.STATUS_CODE_OK, + DATA_SEQUENCE_NUM_1, PEER_EXTENDED_MAC_ADDRESS, SOURCE_END_POINT, DEST_END_POINT, + DATA_PAYLOAD); + + mUwbSessionManager.onDataReceived(TEST_SESSION_ID, UwbUciConstants.STATUS_CODE_OK, + DATA_SEQUENCE_NUM, PEER_EXTENDED_MAC_ADDRESS_2, SOURCE_END_POINT, DEST_END_POINT, + DATA_PAYLOAD); + mUwbSessionManager.onDataReceived(TEST_SESSION_ID, UwbUciConstants.STATUS_CODE_OK, + DATA_SEQUENCE_NUM_1, PEER_EXTENDED_MAC_ADDRESS_2, SOURCE_END_POINT, DEST_END_POINT, + DATA_PAYLOAD); + + verify(mockUwbSession, times(4)).addReceivedDataInfo( + isA(UwbSessionManager.ReceivedDataInfo.class)); + + // Next call onRangeDataNotificationReceived() to process the RANGE_DATA_NTF. + UwbRangingData uwbRangingData1 = UwbTestUtils.generateRangingData( + RANGING_MEASUREMENT_TYPE_OWR_AOA, MAC_ADDRESSING_MODE_EXTENDED, + PEER_EXTENDED_MAC_ADDRESS, UwbUciConstants.STATUS_CODE_OK); + UwbRangingData uwbRangingData2 = UwbTestUtils.generateRangingData( + RANGING_MEASUREMENT_TYPE_OWR_AOA, MAC_ADDRESSING_MODE_EXTENDED, + PEER_EXTENDED_MAC_ADDRESS_2, UwbUciConstants.STATUS_CODE_OK); + Params firaParams = setupFiraParams( + RANGING_DEVICE_ROLE_OBSERVER, Optional.of(ROUND_USAGE_OWR_AOA_MEASUREMENT)); + when(mockUwbSession.getParams()).thenReturn(firaParams); + when(mUwbAdvertiseManager.isPointedTarget(PEER_EXTENDED_MAC_ADDRESS)).thenReturn(true); + when(mUwbAdvertiseManager.isPointedTarget(PEER_EXTENDED_MAC_ADDRESS_2)).thenReturn(true); + when(mockUwbSession.getAllReceivedDataInfo(PEER_EXTENDED_MAC_ADDRESS_LONG)) + .thenReturn(List.of( + buildReceivedDataInfo(PEER_EXTENDED_MAC_ADDRESS_LONG, DATA_SEQUENCE_NUM), + buildReceivedDataInfo( + PEER_EXTENDED_MAC_ADDRESS_LONG, DATA_SEQUENCE_NUM_1))); + when(mockUwbSession.getAllReceivedDataInfo(PEER_EXTENDED_MAC_ADDRESS_2_LONG)) + .thenReturn(List.of( + buildReceivedDataInfo(PEER_EXTENDED_MAC_ADDRESS_2_LONG, DATA_SEQUENCE_NUM), + buildReceivedDataInfo( + PEER_EXTENDED_MAC_ADDRESS_2_LONG, DATA_SEQUENCE_NUM_1))); + mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData1); + mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData2); + + verify(mUwbSessionNotificationManager) + .onRangingResult(eq(mockUwbSession), eq(uwbRangingData1)); + verify(mUwbSessionNotificationManager) + .onRangingResult(eq(mockUwbSession), eq(uwbRangingData2)); + verify(mUwbAdvertiseManager).updateAdvertiseTarget(uwbRangingData1.mRangingOwrAoaMeasure); + verify(mUwbAdvertiseManager).updateAdvertiseTarget(uwbRangingData2.mRangingOwrAoaMeasure); + verify(mUwbSessionNotificationManager, times(2)) + .onDataReceived(eq(mockUwbSession), eq(PEER_EXTENDED_UWB_ADDRESS), + isA(PersistableBundle.class), eq(DATA_PAYLOAD)); + verify(mUwbSessionNotificationManager, times(2)) + .onDataReceived(eq(mockUwbSession), eq(PEER_EXTENDED_UWB_ADDRESS_2), + isA(PersistableBundle.class), eq(DATA_PAYLOAD)); + verify(mUwbAdvertiseManager).removeAdvertiseTarget(PEER_EXTENDED_MAC_ADDRESS_LONG); + verify(mUwbAdvertiseManager).removeAdvertiseTarget(PEER_EXTENDED_MAC_ADDRESS_2_LONG); + verify(mUwbMetrics).logRangingResult(anyInt(), eq(uwbRangingData1)); + verify(mUwbMetrics).logRangingResult(anyInt(), eq(uwbRangingData2)); + } + @Test public void onRangeDataNotificationReceived_owrAoa_CheckPointedTarget_Failed() throws RemoteException { @@ -395,7 +497,7 @@ public class UwbSessionManagerTest { verify(mUwbSessionNotificationManager, never()) .onDataReceived(eq(mockUwbSession), eq(PEER_SHORT_UWB_ADDRESS), isA(PersistableBundle.class), eq(DATA_PAYLOAD)); - verify(mUwbAdvertiseManager, never()).removeAdvertiseTarget(PEER_SHORT_MAC_ADDRESS); + verify(mUwbAdvertiseManager, never()).removeAdvertiseTarget(PEER_SHORT_MAC_ADDRESS_LONG); verify(mUwbMetrics).logRangingResult(anyInt(), eq(uwbRangingData)); } @@ -434,6 +536,7 @@ public class UwbSessionManagerTest { mUwbSessionManager.onDataReceived(TEST_SESSION_ID, UwbUciConstants.STATUS_CODE_OK, DATA_SEQUENCE_NUM, PEER_EXTENDED_MAC_ADDRESS, SOURCE_END_POINT, DEST_END_POINT, DATA_PAYLOAD); + verify(mockUwbSession).addReceivedDataInfo(isA(UwbSessionManager.ReceivedDataInfo.class)); // Next call onRangeDataNotificationReceived() to process the RANGE_DATA_NTF. mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData); @@ -458,6 +561,7 @@ public class UwbSessionManagerTest { mUwbSessionManager.onDataReceived(TEST_SESSION_ID, UwbUciConstants.STATUS_CODE_OK, DATA_SEQUENCE_NUM, PEER_EXTENDED_MAC_ADDRESS, SOURCE_END_POINT, DEST_END_POINT, DATA_PAYLOAD); + verify(mockUwbSession).addReceivedDataInfo(isA(UwbSessionManager.ReceivedDataInfo.class)); // Next call onRangeDataNotificationReceived() to process the RANGE_DATA_NTF (with an // incorrect RangingRoundUsage value). @@ -486,6 +590,7 @@ public class UwbSessionManagerTest { mUwbSessionManager.onDataReceived(TEST_SESSION_ID, UwbUciConstants.STATUS_CODE_OK, DATA_SEQUENCE_NUM, PEER_EXTENDED_MAC_ADDRESS, SOURCE_END_POINT, DEST_END_POINT, DATA_PAYLOAD); + verify(mockUwbSession).addReceivedDataInfo(isA(UwbSessionManager.ReceivedDataInfo.class)); // Next call onRangeDataNotificationReceived() to process the RANGE_DATA_NTF. Params firaParams = setupFiraParams( @@ -514,14 +619,15 @@ public class UwbSessionManagerTest { Params firaParams = setupFiraParams( RANGING_DEVICE_ROLE_OBSERVER, Optional.of(ROUND_USAGE_OWR_AOA_MEASUREMENT)); when(mockUwbSession.getParams()).thenReturn(firaParams); + when(mUwbAdvertiseManager.isPointedTarget(PEER_EXTENDED_MAC_ADDRESS)).thenReturn(true); + when(mockUwbSession.getAllReceivedDataInfo(PEER_EXTENDED_MAC_ADDRESS_LONG)) + .thenReturn(List.of()); mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData); verify(mUwbSessionNotificationManager) .onRangingResult(eq(mockUwbSession), eq(uwbRangingData)); verify(mUwbAdvertiseManager).updateAdvertiseTarget(uwbRangingData.mRangingOwrAoaMeasure); verify(mUwbMetrics).logRangingResult(anyInt(), eq(uwbRangingData)); - - verify(mUwbAdvertiseManager, never()).isPointedTarget(isA(byte[].class)); verifyZeroInteractions(mUwbSessionNotificationManager); } @@ -540,19 +646,21 @@ public class UwbSessionManagerTest { mUwbSessionManager.onDataReceived(TEST_SESSION_ID, UwbUciConstants.STATUS_CODE_OK, DATA_SEQUENCE_NUM, PEER_EXTENDED_MAC_ADDRESS_2, SOURCE_END_POINT, DEST_END_POINT, DATA_PAYLOAD); + verify(mockUwbSession).addReceivedDataInfo(isA(UwbSessionManager.ReceivedDataInfo.class)); // Next call onRangeDataNotificationReceived() to process the RANGE_DATA_NTF. Params firaParams = setupFiraParams( RANGING_DEVICE_ROLE_OBSERVER, Optional.of(ROUND_USAGE_OWR_AOA_MEASUREMENT)); when(mockUwbSession.getParams()).thenReturn(firaParams); + when(mUwbAdvertiseManager.isPointedTarget(PEER_EXTENDED_MAC_ADDRESS)).thenReturn(true); + when(mockUwbSession.getAllReceivedDataInfo(PEER_EXTENDED_MAC_ADDRESS_LONG)) + .thenReturn(List.of()); mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData); verify(mUwbSessionNotificationManager) .onRangingResult(eq(mockUwbSession), eq(uwbRangingData)); verify(mUwbAdvertiseManager).updateAdvertiseTarget(uwbRangingData.mRangingOwrAoaMeasure); verify(mUwbMetrics).logRangingResult(anyInt(), eq(uwbRangingData)); - - verify(mUwbAdvertiseManager, never()).isPointedTarget(isA(byte[].class)); verifyZeroInteractions(mUwbSessionNotificationManager); } @@ -565,30 +673,33 @@ public class UwbSessionManagerTest { when(mockUwbSession.getWaitObj()).thenReturn(mock(WaitObj.class)); doReturn(mockUwbSession) .when(mUwbSessionManager).getUwbSession(eq(TEST_SESSION_ID)); + UwbSession mockUwbSession2 = mock(UwbSession.class); + when(mockUwbSession2.getWaitObj()).thenReturn(mock(WaitObj.class)); + doReturn(mockUwbSession2) + .when(mUwbSessionManager).getUwbSession(eq(TEST_SESSION_ID_2)); // onDataReceived() called for a different UwbSessionID, which should be equivalent to it // not being called. mUwbSessionManager.onDataReceived(TEST_SESSION_ID_2, UwbUciConstants.STATUS_CODE_OK, DATA_SEQUENCE_NUM, PEER_EXTENDED_MAC_ADDRESS, SOURCE_END_POINT, DEST_END_POINT, DATA_PAYLOAD); + verify(mockUwbSession2).addReceivedDataInfo(isA(UwbSessionManager.ReceivedDataInfo.class)); - // Next call onRangeDataNotificationReceived() to process the RANGE_DATA_NTF. - UwbSession mockUwbSession2 = mock(UwbSession.class); - when(mockUwbSession2.getWaitObj()).thenReturn(mock(WaitObj.class)); - doReturn(mockUwbSession2) - .when(mUwbSessionManager).getUwbSession(eq(TEST_SESSION_ID_2)); - + // Next call onRangeDataNotificationReceived() to process the RANGE_DATA_NTF. Setup such + // that there is no ReceivedDataInfo returned for the UwbSession (to simulate the test + // scenario). Params firaParams = setupFiraParams( RANGING_DEVICE_ROLE_OBSERVER, Optional.of(ROUND_USAGE_OWR_AOA_MEASUREMENT)); when(mockUwbSession.getParams()).thenReturn(firaParams); + when(mUwbAdvertiseManager.isPointedTarget(PEER_EXTENDED_MAC_ADDRESS)).thenReturn(true); + when(mockUwbSession.getAllReceivedDataInfo(PEER_EXTENDED_MAC_ADDRESS_LONG)) + .thenReturn(List.of()); mUwbSessionManager.onRangeDataNotificationReceived(uwbRangingData); verify(mUwbSessionNotificationManager) .onRangingResult(eq(mockUwbSession), eq(uwbRangingData)); verify(mUwbAdvertiseManager).updateAdvertiseTarget(uwbRangingData.mRangingOwrAoaMeasure); verify(mUwbMetrics).logRangingResult(anyInt(), eq(uwbRangingData)); - - verify(mUwbAdvertiseManager, never()).isPointedTarget(isA(byte[].class)); verifyZeroInteractions(mUwbSessionNotificationManager); } @@ -605,6 +716,7 @@ public class UwbSessionManagerTest { mUwbSessionManager.onDataReceived(TEST_SESSION_ID, UwbUciConstants.STATUS_CODE_OK, DATA_SEQUENCE_NUM, PEER_EXTENDED_MAC_ADDRESS, SOURCE_END_POINT, DEST_END_POINT, DATA_PAYLOAD); + verify(mockUwbSession).addReceivedDataInfo(isA(UwbSessionManager.ReceivedDataInfo.class)); // Setup isPointedTarget() to return false. when(mUwbAdvertiseManager.isPointedTarget(PEER_EXTENDED_MAC_ADDRESS)).thenReturn(false); @@ -613,7 +725,7 @@ public class UwbSessionManagerTest { verify(mUwbSessionNotificationManager) .onRangingResult(eq(mockUwbSession), eq(uwbRangingData)); verify(mUwbMetrics).logRangingResult(anyInt(), eq(uwbRangingData)); - verify(mUwbAdvertiseManager, never()).removeAdvertiseTarget(isA(byte[].class)); + verify(mUwbAdvertiseManager, never()).removeAdvertiseTarget(isA(Long.class)); verifyZeroInteractions(mUwbSessionNotificationManager); } @@ -677,7 +789,7 @@ public class UwbSessionManagerTest { doReturn(true).when(mUwbSessionManager).isExistedSession(anyInt()); mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, mock(SessionHandle.class), - TEST_SESSION_ID, "any", mock(Params.class), mockRangingCallbacks, + TEST_SESSION_ID, TEST_SESSION_TYPE, "any", mock(Params.class), mockRangingCallbacks, TEST_CHIP_ID); verify(mockRangingCallbacks).onRangingOpenFailed( @@ -692,7 +804,7 @@ public class UwbSessionManagerTest { IUwbRangingCallbacks mockRangingCallbacks = mock(IUwbRangingCallbacks.class); mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, mock(SessionHandle.class), - TEST_SESSION_ID, "any", mock(Params.class), mockRangingCallbacks, + TEST_SESSION_ID, TEST_SESSION_TYPE, "any", mock(Params.class), mockRangingCallbacks, TEST_CHIP_ID); verify(mockRangingCallbacks).onRangingOpenFailed(any(), anyInt(), any()); @@ -709,15 +821,16 @@ public class UwbSessionManagerTest { IBinder mockBinder = mock(IBinder.class); UwbSession uwbSession = spy( mUwbSessionManager.new UwbSession(ATTRIBUTION_SOURCE, mockSessionHandle, - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, mockParams, + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, mockParams, mockRangingCallbacks, TEST_CHIP_ID)); doReturn(mockBinder).when(uwbSession).getBinder(); doReturn(uwbSession).when(mUwbSessionManager).createUwbSession(any(), any(), anyInt(), - anyString(), any(), any(), anyString()); + anyByte(), anyString(), any(), any(), anyString()); doThrow(new RemoteException()).when(mockBinder).linkToDeath(any(), anyInt()); mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, mockSessionHandle, TEST_SESSION_ID, - FiraParams.PROTOCOL_NAME, mockParams, mockRangingCallbacks, TEST_CHIP_ID); + TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, mockParams, mockRangingCallbacks, + TEST_CHIP_ID); verify(uwbSession).binderDied(); verify(mockRangingCallbacks).onRangingOpenFailed(any(), anyInt(), any()); @@ -737,14 +850,15 @@ public class UwbSessionManagerTest { UwbSession uwbSession = spy( mUwbSessionManager.new UwbSession(ATTRIBUTION_SOURCE, mockSessionHandle, - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, mockParams, + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, mockParams, mockRangingCallbacks, TEST_CHIP_ID)); doReturn(mockBinder).when(uwbSession).getBinder(); doReturn(uwbSession).when(mUwbSessionManager).createUwbSession(any(), any(), anyInt(), - anyString(), any(), any(), anyString()); + anyByte(), anyString(), any(), any(), anyString()); mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, mockSessionHandle, TEST_SESSION_ID, - FiraParams.PROTOCOL_NAME, mockParams, mockRangingCallbacks, TEST_CHIP_ID); + TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, mockParams, mockRangingCallbacks, + TEST_CHIP_ID); verify(uwbSession, never()).binderDied(); verify(mockRangingCallbacks, never()).onRangingOpenFailed(any(), anyInt(), any()); @@ -767,14 +881,15 @@ public class UwbSessionManagerTest { UwbSession uwbSession = spy( mUwbSessionManager.new UwbSession(ATTRIBUTION_SOURCE, mockSessionHandle, - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, mockParams, + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, mockParams, mockRangingCallbacks, TEST_CHIP_ID)); doReturn(mockBinder).when(uwbSession).getBinder(); doReturn(uwbSession).when(mUwbSessionManager).createUwbSession(any(), any(), anyInt(), - anyString(), any(), any(), anyString()); + anyByte(), anyString(), any(), any(), anyString()); mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, mockSessionHandle, TEST_SESSION_ID, - FiraParams.PROTOCOL_NAME, mockParams, mockRangingCallbacks, TEST_CHIP_ID); + TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, mockParams, mockRangingCallbacks, + TEST_CHIP_ID); assertThat(uwbSession.getControleeList().size() == 1 && uwbSession.getControleeList().get(0).getUwbAddress().equals(UWB_DEST_ADDRESS)) @@ -888,12 +1003,13 @@ public class UwbSessionManagerTest { // Setup the UwbSession to have the peer device's MacAddress stored (which happens when // a valid RANGE_DATA_NTF with an OWR AoA Measurement is received). - doReturn(PEER_EXTENDED_MAC_ADDRESS).when(mockUwbSession).getRemoteMacAddress(); + doReturn(Set.of(PEER_EXTENDED_MAC_ADDRESS_LONG)).when(mockUwbSession) + .getRemoteMacAddressList(); mUwbSessionManager.stopRanging(mock(SessionHandle.class)); mTestLooper.dispatchNext(); - verify(mUwbAdvertiseManager).removeAdvertiseTarget(PEER_EXTENDED_MAC_ADDRESS); + verify(mUwbAdvertiseManager).removeAdvertiseTarget(PEER_EXTENDED_MAC_ADDRESS_LONG); } @Test @@ -1083,11 +1199,11 @@ public class UwbSessionManagerTest { Params params = setupFiraParams(); UwbSession uwbSession = spy( mUwbSessionManager.new UwbSession(attributionSource, mockSessionHandle, - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, params, mockRangingCallbacks, - TEST_CHIP_ID)); + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, params, + mockRangingCallbacks, TEST_CHIP_ID)); doReturn(mockBinder).when(uwbSession).getBinder(); doReturn(uwbSession).when(mUwbSessionManager).createUwbSession(any(), any(), anyInt(), - anyString(), any(), any(), anyString()); + anyByte(), anyString(), any(), any(), anyString()); doReturn(mock(WaitObj.class)).when(uwbSession).getWaitObj(); return uwbSession; @@ -1107,6 +1223,7 @@ public class UwbSessionManagerTest { UWB_DEST_ADDRESS)) .setProtocolVersion(new FiraProtocolVersion(1, 0)) .setSessionId(10) + .setSessionType(SESSION_TYPE_RANGING) .setDeviceType(FiraParams.RANGING_DEVICE_TYPE_CONTROLLER) .setDeviceRole(deviceRole) .setMultiNodeMode(FiraParams.MULTI_NODE_MODE_UNICAST) @@ -1145,11 +1262,11 @@ public class UwbSessionManagerTest { IBinder mockBinder = mock(IBinder.class); UwbSession uwbSession = spy( mUwbSessionManager.new UwbSession(ATTRIBUTION_SOURCE, mockSessionHandle, - TEST_SESSION_ID, CccParams.PROTOCOL_NAME, params, mockRangingCallbacks, - TEST_CHIP_ID)); + TEST_SESSION_ID, TEST_SESSION_TYPE, CccParams.PROTOCOL_NAME, params, + mockRangingCallbacks, TEST_CHIP_ID)); doReturn(mockBinder).when(uwbSession).getBinder(); doReturn(uwbSession).when(mUwbSessionManager).createUwbSession(any(), any(), anyInt(), - anyString(), any(), any(), anyString()); + anyByte(), anyString(), any(), any(), anyString()); doReturn(mock(WaitObj.class)).when(uwbSession).getWaitObj(); return uwbSession; @@ -1168,7 +1285,7 @@ public class UwbSessionManagerTest { mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(), - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks(), TEST_CHIP_ID); mTestLooper.dispatchAll(); @@ -1193,7 +1310,7 @@ public class UwbSessionManagerTest { mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(), - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks(), TEST_CHIP_ID); mTestLooper.dispatchAll(); @@ -1217,7 +1334,7 @@ public class UwbSessionManagerTest { mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(), - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks(), TEST_CHIP_ID); mTestLooper.dispatchAll(); @@ -1241,7 +1358,7 @@ public class UwbSessionManagerTest { mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(), - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks(), TEST_CHIP_ID); mTestLooper.dispatchAll(); @@ -1265,7 +1382,7 @@ public class UwbSessionManagerTest { mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(), - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks(), TEST_CHIP_ID); mTestLooper.dispatchAll(); @@ -1289,7 +1406,7 @@ public class UwbSessionManagerTest { mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(), - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks(), TEST_CHIP_ID); mTestLooper.dispatchAll(); @@ -1308,7 +1425,7 @@ public class UwbSessionManagerTest { UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE); mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(), - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks(), TEST_CHIP_ID); // OPEN_RANGING message scheduled. @@ -1323,7 +1440,7 @@ public class UwbSessionManagerTest { UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE); mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(), - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks(), TEST_CHIP_ID); verify(uwbSession.getIUwbRangingCallbacks()).onRangingOpenFailed( @@ -1347,7 +1464,7 @@ public class UwbSessionManagerTest { UwbSession uwbSession = setUpUwbSessionForExecution(attributionSource); mUwbSessionManager.initSession(attributionSource, uwbSession.getSessionHandle(), - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks(), TEST_CHIP_ID); return uwbSession; } @@ -1461,7 +1578,7 @@ public class UwbSessionManagerTest { .build(); UwbSession uwbSession = setUpUwbSessionForExecution(attributionSource); mUwbSessionManager.initSession(attributionSource, uwbSession.getSessionHandle(), - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks(), TEST_CHIP_ID); verify(uwbSession.getIUwbRangingCallbacks()).onRangingOpenFailed( @@ -1473,7 +1590,7 @@ public class UwbSessionManagerTest { private UwbSession prepareExistingUwbSession() throws Exception { UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE); mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(), - TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + TEST_SESSION_ID, TEST_SESSION_TYPE, FiraParams.PROTOCOL_NAME, uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks(), TEST_CHIP_ID); mTestLooper.nextMessage(); // remove the OPEN_RANGING msg; @@ -1485,7 +1602,7 @@ public class UwbSessionManagerTest { private UwbSession prepareExistingCccUwbSession() throws Exception { UwbSession uwbSession = setUpCccUwbSessionForExecution(); mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(), - TEST_SESSION_ID, CccParams.PROTOCOL_NAME, + TEST_SESSION_ID, TEST_SESSION_TYPE, CccParams.PROTOCOL_NAME, uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks(), TEST_CHIP_ID); mTestLooper.nextMessage(); // remove the OPEN_RANGING msg; @@ -1713,6 +1830,42 @@ public class UwbSessionManagerTest { } @Test + public void session_receivedDataInfo() throws Exception { + UwbSession uwbSession = prepareExistingUwbSession(); + + // Setup the UwbSession to have multiple data packets (being received) for multiple remote + // devices. This includes some duplicate packets (same sequence number from same remote + // device), which should be ignored. + UwbSessionManager.ReceivedDataInfo deviceOnePacketOne = buildReceivedDataInfo( + PEER_EXTENDED_MAC_ADDRESS_LONG, DATA_SEQUENCE_NUM); + UwbSessionManager.ReceivedDataInfo deviceOnePacketTwo = buildReceivedDataInfo( + PEER_EXTENDED_MAC_ADDRESS_LONG, DATA_SEQUENCE_NUM_1); + UwbSessionManager.ReceivedDataInfo deviceTwoPacketOne = buildReceivedDataInfo( + PEER_EXTENDED_MAC_ADDRESS_2_LONG, DATA_SEQUENCE_NUM); + UwbSessionManager.ReceivedDataInfo deviceTwoPacketTwo = buildReceivedDataInfo( + PEER_EXTENDED_MAC_ADDRESS_2_LONG, DATA_SEQUENCE_NUM_1); + + uwbSession.addReceivedDataInfo(deviceOnePacketOne); + uwbSession.addReceivedDataInfo(deviceOnePacketTwo); + uwbSession.addReceivedDataInfo(deviceOnePacketOne); + + uwbSession.addReceivedDataInfo(deviceTwoPacketOne); + uwbSession.addReceivedDataInfo(deviceTwoPacketTwo); + uwbSession.addReceivedDataInfo(deviceTwoPacketOne); + + // Verify that the first call to getAllReceivedDataInfo() for a device returns all it's + // received packets, and the second call receives an empty list. + assertThat(uwbSession.getAllReceivedDataInfo(PEER_EXTENDED_MAC_ADDRESS_LONG)).isEqualTo( + List.of(deviceOnePacketOne, deviceOnePacketTwo)); + assertThat(uwbSession.getAllReceivedDataInfo(PEER_EXTENDED_MAC_ADDRESS_LONG)).isEqualTo( + List.of()); + assertThat(uwbSession.getAllReceivedDataInfo(PEER_EXTENDED_MAC_ADDRESS_2_LONG)).isEqualTo( + List.of(deviceTwoPacketOne, deviceTwoPacketTwo)); + assertThat(uwbSession.getAllReceivedDataInfo(PEER_EXTENDED_MAC_ADDRESS_2_LONG)).isEqualTo( + List.of()); + } + + @Test public void execStartCccRanging_success() throws Exception { UwbSession uwbSession = prepareExistingCccUwbSession(); // set up for start ranging @@ -1822,7 +1975,8 @@ public class UwbSessionManagerTest { when(mNativeUwbManager.sendData(eq(TEST_SESSION_ID), eq(PEER_EXTENDED_UWB_ADDRESS.toBytes()), eq(UwbUciConstants.UWB_DESTINATION_END_POINT_HOST), eq(DATA_SEQUENCE_NUM), - eq(DATA_PAYLOAD))).thenReturn((byte) UwbUciConstants.STATUS_CODE_OK); + eq(DATA_PAYLOAD), eq(TEST_CHIP_ID))) + .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK); mUwbSessionManager.sendData(uwbSession.getSessionHandle(), PEER_EXTENDED_UWB_ADDRESS, PERSISTABLE_BUNDLE, DATA_PAYLOAD); @@ -1831,12 +1985,52 @@ public class UwbSessionManagerTest { verify(mNativeUwbManager).sendData(eq(TEST_SESSION_ID), eq(PEER_EXTENDED_UWB_ADDRESS.toBytes()), eq(UwbUciConstants.UWB_DESTINATION_END_POINT_HOST), eq(DATA_SEQUENCE_NUM), - eq(DATA_PAYLOAD)); + eq(DATA_PAYLOAD), eq(TEST_CHIP_ID)); verify(mUwbSessionNotificationManager).onDataSent( eq(uwbSession), eq(PEER_EXTENDED_UWB_ADDRESS), eq(PERSISTABLE_BUNDLE)); } @Test + public void sendData_success_sequenceNumberRollover() throws Exception { + UwbSession uwbSession = prepareExistingUwbSession(); + + // Setup the UwbSession to start ranging (and move it to active state). + doReturn(UwbUciConstants.UWB_SESSION_STATE_IDLE).when(uwbSession).getSessionState(); + when(mNativeUwbManager.startRanging(eq(TEST_SESSION_ID), anyString())) + .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK); + + mUwbSessionManager.startRanging( + uwbSession.getSessionHandle(), uwbSession.getParams()); + mTestLooper.dispatchAll(); + doReturn(UwbUciConstants.UWB_SESSION_STATE_ACTIVE).when(uwbSession).getSessionState(); + + clearInvocations(mNativeUwbManager); + + // Send 257 data packets on the UWB session, so that the UCI sequence number rolls over, + // back to 0. + when(mNativeUwbManager.sendData(anyInt(), any(), anyByte(), anyByte(), any(), anyString())) + .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK); + + for (int i = 0; i <= 256; i++) { + mUwbSessionManager.sendData(uwbSession.getSessionHandle(), PEER_EXTENDED_UWB_ADDRESS, + PERSISTABLE_BUNDLE, DATA_PAYLOAD); + mTestLooper.dispatchNext(); + } + + // Verify that there are 257 calls to mNativeUwbManager.sendData(), with the important + // thing here being that there should be 2 calls for sequence_number = 0, and 1 call for all + // the other sequence number values [1-255]. + for (int i = 0; i < 256; i++) { + int expectedCount = (i == 0) ? 2 : 1; + verify(mNativeUwbManager, times(expectedCount)).sendData(eq(TEST_SESSION_ID), + eq(PEER_EXTENDED_UWB_ADDRESS.toBytes()), + eq(UwbUciConstants.UWB_DESTINATION_END_POINT_HOST), eq((byte) i), + eq(DATA_PAYLOAD), eq(TEST_CHIP_ID)); + } + verifyNoMoreInteractions(mNativeUwbManager); + } + + @Test public void sendData_missingSessionHandle() throws Exception { mUwbSessionManager.sendData( null /* sessionHandle */, PEER_EXTENDED_UWB_ADDRESS, PERSISTABLE_BUNDLE, @@ -1846,7 +2040,7 @@ public class UwbSessionManagerTest { verify(mNativeUwbManager, never()).sendData( eq(TEST_SESSION_ID), eq(PEER_EXTENDED_UWB_ADDRESS.toBytes()), eq(UwbUciConstants.UWB_DESTINATION_END_POINT_HOST), eq(DATA_SEQUENCE_NUM), - eq(DATA_PAYLOAD)); + eq(DATA_PAYLOAD), eq(TEST_CHIP_ID)); verify(mUwbSessionNotificationManager).onDataSendFailed( eq(null), eq(PEER_EXTENDED_UWB_ADDRESS), eq(UwbUciConstants.STATUS_CODE_ERROR_SESSION_NOT_EXIST), eq(PERSISTABLE_BUNDLE)); @@ -1865,7 +2059,7 @@ public class UwbSessionManagerTest { verify(mNativeUwbManager, never()).sendData( eq(TEST_SESSION_ID), eq(PEER_EXTENDED_UWB_ADDRESS.toBytes()), eq(UwbUciConstants.UWB_DESTINATION_END_POINT_HOST), eq(DATA_SEQUENCE_NUM), - eq(DATA_PAYLOAD)); + eq(DATA_PAYLOAD), eq(TEST_CHIP_ID)); verify(mUwbSessionNotificationManager).onDataSendFailed( eq(null), eq(PEER_EXTENDED_UWB_ADDRESS), eq(UwbUciConstants.STATUS_CODE_ERROR_SESSION_NOT_EXIST), eq(PERSISTABLE_BUNDLE)); @@ -1885,7 +2079,7 @@ public class UwbSessionManagerTest { verify(mNativeUwbManager, never()).sendData( eq(TEST_SESSION_ID), eq(PEER_EXTENDED_UWB_ADDRESS.toBytes()), eq(UwbUciConstants.UWB_DESTINATION_END_POINT_HOST), eq(DATA_SEQUENCE_NUM), - eq(DATA_PAYLOAD)); + eq(DATA_PAYLOAD), eq(TEST_CHIP_ID)); verify(mUwbSessionNotificationManager).onDataSendFailed( eq(uwbSession), eq(PEER_EXTENDED_UWB_ADDRESS), eq(UwbUciConstants.STATUS_CODE_FAILED), eq(PERSISTABLE_BUNDLE)); @@ -1913,7 +2107,7 @@ public class UwbSessionManagerTest { verify(mNativeUwbManager, never()).sendData( eq(TEST_SESSION_ID), eq(PEER_EXTENDED_UWB_ADDRESS.toBytes()), eq(UwbUciConstants.UWB_DESTINATION_END_POINT_HOST), eq(DATA_SEQUENCE_NUM), - eq(DATA_PAYLOAD)); + eq(DATA_PAYLOAD), eq(TEST_CHIP_ID)); verify(mUwbSessionNotificationManager).onDataSendFailed( eq(uwbSession), eq(PEER_EXTENDED_UWB_ADDRESS), eq(UwbUciConstants.STATUS_CODE_INVALID_PARAM), eq(PERSISTABLE_BUNDLE)); @@ -1941,7 +2135,7 @@ public class UwbSessionManagerTest { verify(mNativeUwbManager, never()).sendData( eq(TEST_SESSION_ID), eq(PEER_EXTENDED_UWB_ADDRESS.toBytes()), eq(UwbUciConstants.UWB_DESTINATION_END_POINT_HOST), eq(DATA_SEQUENCE_NUM), - eq(DATA_PAYLOAD)); + eq(DATA_PAYLOAD), eq(TEST_CHIP_ID)); verify(mUwbSessionNotificationManager).onDataSendFailed( eq(uwbSession), eq(null), eq(UwbUciConstants.STATUS_CODE_INVALID_PARAM), eq(PERSISTABLE_BUNDLE)); @@ -1965,7 +2159,8 @@ public class UwbSessionManagerTest { when(mNativeUwbManager.sendData(eq(TEST_SESSION_ID), eq(PEER_EXTENDED_UWB_ADDRESS.toBytes()), eq(UwbUciConstants.UWB_DESTINATION_END_POINT_HOST), eq(DATA_SEQUENCE_NUM), - eq(DATA_PAYLOAD))).thenReturn((byte) UwbUciConstants.STATUS_CODE_FAILED); + eq(DATA_PAYLOAD), eq(TEST_CHIP_ID))) + .thenReturn((byte) UwbUciConstants.STATUS_CODE_FAILED); mUwbSessionManager.sendData(uwbSession.getSessionHandle(), PEER_EXTENDED_UWB_ADDRESS, PERSISTABLE_BUNDLE, DATA_PAYLOAD); @@ -1974,7 +2169,7 @@ public class UwbSessionManagerTest { verify(mNativeUwbManager).sendData(eq(TEST_SESSION_ID), eq(PEER_EXTENDED_UWB_ADDRESS.toBytes()), eq(UwbUciConstants.UWB_DESTINATION_END_POINT_HOST), eq(DATA_SEQUENCE_NUM), - eq(DATA_PAYLOAD)); + eq(DATA_PAYLOAD), eq(TEST_CHIP_ID)); verify(mUwbSessionNotificationManager).onDataSendFailed( eq(uwbSession), eq(PEER_EXTENDED_UWB_ADDRESS), eq(UwbUciConstants.STATUS_CODE_FAILED), eq(PERSISTABLE_BUNDLE)); @@ -2417,7 +2612,8 @@ public class UwbSessionManagerTest { // Setup the UwbSession to have the peer device's MacAddress stored (which happens when // a valid RANGE_DATA_NTF with an OWR AoA Measurement is received). - doReturn(PEER_EXTENDED_MAC_ADDRESS).when(mockUwbSession).getRemoteMacAddress(); + doReturn(Set.of(PEER_EXTENDED_MAC_ADDRESS_LONG)).when(mockUwbSession) + .getRemoteMacAddressList(); // Call deInitSession(). IBinder mockBinder = mock(IBinder.class); @@ -2430,7 +2626,7 @@ public class UwbSessionManagerTest { mTestLooper.dispatchNext(); - verify(mUwbAdvertiseManager).removeAdvertiseTarget(PEER_EXTENDED_MAC_ADDRESS); + verify(mUwbAdvertiseManager).removeAdvertiseTarget(PEER_EXTENDED_MAC_ADDRESS_LONG); } @Test @@ -2470,7 +2666,7 @@ public class UwbSessionManagerTest { verify(mUwbSessionNotificationManager).onRangingClosed( eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED)); - verify(mUwbAdvertiseManager, never()).removeAdvertiseTarget(isA(byte[].class)); + verify(mUwbAdvertiseManager, never()).removeAdvertiseTarget(isA(Long.class)); verify(mUwbMetrics).logRangingCloseEvent( eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED)); assertThat(mUwbSessionManager.getSessionCount()).isEqualTo(0); @@ -2558,7 +2754,7 @@ public class UwbSessionManagerTest { eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_OK)); assertThat(mUwbSessionManager.getSessionCount()).isEqualTo(0); - verify(mUwbAdvertiseManager).removeAdvertiseTarget(isA(byte[].class)); + verify(mUwbAdvertiseManager).removeAdvertiseTarget(isA(Long.class)); } @Test @@ -2573,4 +2769,21 @@ public class UwbSessionManagerTest { eq(uwbSession), eq(UwbUciConstants.STATUS_CODE_FAILED)); assertThat(mUwbSessionManager.getSessionCount()).isEqualTo(0); } + + private UwbSessionManager.ReceivedDataInfo buildReceivedDataInfo(long macAddress) { + return buildReceivedDataInfo(macAddress, DATA_SEQUENCE_NUM); + } + + private UwbSessionManager.ReceivedDataInfo buildReceivedDataInfo( + long macAddress, long sequenceNum) { + UwbSessionManager.ReceivedDataInfo info = new UwbSessionManager.ReceivedDataInfo(); + info.sessionId = TEST_SESSION_ID; + info.status = STATUS_CODE_OK; + info.sequenceNum = sequenceNum; + info.address = macAddress; + info.sourceEndPoint = SOURCE_END_POINT; + info.destEndPoint = DEST_END_POINT; + info.payload = DATA_PAYLOAD; + return info; + } } diff --git a/service/tests/src/com/android/server/uwb/UwbSessionNotificationManagerTest.java b/service/tests/src/com/android/server/uwb/UwbSessionNotificationManagerTest.java index 5b3810da..32bdad81 100644 --- a/service/tests/src/com/android/server/uwb/UwbSessionNotificationManagerTest.java +++ b/service/tests/src/com/android/server/uwb/UwbSessionNotificationManagerTest.java @@ -315,6 +315,14 @@ public class UwbSessionNotificationManagerTest { } @Test + public void testOnRangingResult_badRangingDataForOwrAoa() throws Exception { + UwbRangingData testRangingData = UwbTestUtils.generateBadOwrAoaMeasurementRangingData( + MAC_ADDRESSING_MODE_SHORT, PEER_SHORT_MAC_ADDRESS); + mUwbSessionNotificationManager.onRangingResult(mUwbSession, testRangingData); + verify(mIUwbRangingCallbacks).onRangingResult(mSessionHandle, null); + } + + @Test public void testOnRangingOpened() throws Exception { mUwbSessionNotificationManager.onRangingOpened(mUwbSession); diff --git a/service/tests/src/com/android/server/uwb/advertisement/UwbAdvertiseManagerTest.java b/service/tests/src/com/android/server/uwb/advertisement/UwbAdvertiseManagerTest.java index a4bd7b03..f182ae94 100644 --- a/service/tests/src/com/android/server/uwb/advertisement/UwbAdvertiseManagerTest.java +++ b/service/tests/src/com/android/server/uwb/advertisement/UwbAdvertiseManagerTest.java @@ -322,11 +322,11 @@ public class UwbAdvertiseManagerTest { assertNull(mUwbAdvertiseManager.getAdvertiseTarget(TEST_MAC_ADDRESS_B_INT)); // Call removeAdvertiseTarget() for the device and verify that it has been removed. - mUwbAdvertiseManager.removeAdvertiseTarget(TEST_MAC_ADDRESS_A); + mUwbAdvertiseManager.removeAdvertiseTarget(TEST_MAC_ADDRESS_A_LONG); assertNull(mUwbAdvertiseManager.getAdvertiseTarget(TEST_MAC_ADDRESS_A_LONG)); // Call removeAdvertiseTarget() for a device that doesn't exist and verify no exceptions. - mUwbAdvertiseManager.removeAdvertiseTarget(TEST_MAC_ADDRESS_B); + mUwbAdvertiseManager.removeAdvertiseTarget(TEST_MAC_ADDRESS_B_INT); assertNull(mUwbAdvertiseManager.getAdvertiseTarget(TEST_MAC_ADDRESS_B_INT)); } diff --git a/service/tests/src/com/android/server/uwb/correction/TestHelpers.java b/service/tests/src/com/android/server/uwb/correction/TestHelpers.java index 7a5b3f01..fe10539c 100644 --- a/service/tests/src/com/android/server/uwb/correction/TestHelpers.java +++ b/service/tests/src/com/android/server/uwb/correction/TestHelpers.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.junit.Assert; public final class TestHelpers { private TestHelpers() {} + // Asserts that a value is within 0.001, to account for floating point rounding errors. public static void assertClose(double v, double c) { Assert.assertTrue(abs(v - c) < 0.001); } diff --git a/service/tests/src/com/android/server/uwb/correction/UwbFilterEngineTest.java b/service/tests/src/com/android/server/uwb/correction/UwbFilterEngineTest.java new file mode 100644 index 00000000..02ada3e7 --- /dev/null +++ b/service/tests/src/com/android/server/uwb/correction/UwbFilterEngineTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction; + +import static com.android.server.uwb.correction.TestHelpers.assertClose; + +import static com.google.common.truth.Truth.assertThat; + +import com.android.server.uwb.correction.filtering.NullFilter; +import com.android.server.uwb.correction.filtering.PositionFilterImpl; +import com.android.server.uwb.correction.math.Pose; +import com.android.server.uwb.correction.math.Quaternion; +import com.android.server.uwb.correction.math.SphericalVector; +import com.android.server.uwb.correction.math.Vector3; +import com.android.server.uwb.correction.pose.NullPoseSource; +import com.android.server.uwb.correction.primers.NullPrimer; + +import org.junit.Test; + +public class UwbFilterEngineTest { + + @Test + public void basic() { + UwbFilterEngine engine = new UwbFilterEngine.Builder().build(); + engine.add(SphericalVector.fromRadians(1, 1.2f, 1.3f).toSparse()); + SphericalVector currentVector = engine.compute(); + assertThat(currentVector.azimuth).isEqualTo(1); + assertThat(currentVector.elevation).isEqualTo(1.2f); + assertThat(currentVector.distance).isEqualTo(1.3f); + engine.close(); + } + + @Test + public void poseChanges() { + NullPoseSource poseSource = new NullPoseSource(); + UwbFilterEngine engine = new UwbFilterEngine.Builder() + .setFilter( + new PositionFilterImpl(new NullFilter(), new NullFilter(), new NullFilter())) + .setPoseSource(poseSource) + .build(); + + poseSource.changePose(Pose.IDENTITY); + engine.add(SphericalVector.fromRadians(0.7f, 1.2f, 1.3f).toSparse()); + + // Check initial state. + SphericalVector currentVector = engine.compute(); + assertThat(currentVector.azimuth).isEqualTo(0.7f); + assertThat(currentVector.elevation).isEqualTo(1.2f); + assertThat(currentVector.distance).isEqualTo(1.3f); + + // Turn left. + poseSource.changePose( + new Pose(Vector3.ORIGIN, Quaternion.yawPitchRoll(-0.5f, 0, 0)) + ); + currentVector = engine.compute(); + + // See if the azimuth is to our right now that we turned left. + assertClose(currentVector.azimuth, 0.7f - 0.5f); + assertClose(currentVector.elevation, 1.2f); + assertClose(currentVector.distance, 1.3f); + + Pose newPose = engine.getPose(); + assertThat(newPose.translation.lengthSquared()).isEqualTo(0); + assertClose(newPose.rotation.toYawPitchRoll().x, -0.5f); + + engine.close(); + } + + @Test + public void primerTest() { + NullPoseSource poseSource = new NullPoseSource(); + NullPrimer primer = new NullPrimer(); + UwbFilterEngine engine = new UwbFilterEngine.Builder() + .addPrimer(primer) + .setFilter( + new PositionFilterImpl(new NullFilter(), new NullFilter(), new NullFilter())) + .setPoseSource(poseSource) + .build(); + + poseSource.changePose(Pose.IDENTITY); + engine.add(SphericalVector.fromRadians(-0.7f, 0, 1.3f).toSparse()); + + // Check initial state. + SphericalVector currentVector = engine.compute(); + assertThat(currentVector.azimuth).isEqualTo(0.7f); // Primer would make this positive. + assertThat(currentVector.elevation).isEqualTo(0f); + assertThat(currentVector.distance).isEqualTo(1.3f); + + engine.add(SphericalVector.fromRadians(0f, 0, 1.3f).toSparse()); + + // Look down. + poseSource.changePose( + new Pose(Vector3.ORIGIN, Quaternion.yawPitchRoll(0, 1, 0)) + ); + + // Generate a new measurement that doesn't have elevation or distance. + engine.add( + SphericalVector.fromRadians(0f, 0f, 0f) + .toSparse(true, false, false) + ); + + // Expect the predicted elevation based on the pose change. Distance should be unaffected. + currentVector = engine.compute(); + assertClose(currentVector.azimuth, 0.0f); + assertClose(currentVector.elevation, -1f); + assertClose(currentVector.distance, 1.3f); + + engine.close(); + } +} diff --git a/service/tests/src/com/android/server/uwb/correction/filtering/MAFilterTest.java b/service/tests/src/com/android/server/uwb/correction/filtering/MAFilterTest.java new file mode 100644 index 00000000..9a4e059a --- /dev/null +++ b/service/tests/src/com/android/server/uwb/correction/filtering/MAFilterTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.filtering; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; + +public class MAFilterTest { + + @Test + public void averageTest() { + MAFilter filter = new MAFilter(3, 1); + filter.add(1); + filter.add(2); + filter.add(3); + assertThat(filter.getResult().value).isEqualTo((1 + 2 + 3) / 3f); + } + + // Test when the sliding window is full and it needs to slide. + @Test + public void slideTest() { + MAFilter filter = new MAFilter(3, 1); + filter.add(1); + filter.add(2); + filter.add(3); + filter.add(4); + assertThat(filter.getResult().value).isEqualTo((2 + 3 + 4) / 3f); + } + + @Test + public void remapTest() { + MAFilter filter = new MAFilter(3, 1); + filter.add(1); + filter.add(2); + filter.add(3); + filter.compensate(66); + assertThat(filter.getResult().value).isEqualTo((1 + 2 + 3) / 3f + 66); + } + + // Ensures that a pure median with an even-sized window uses the center two values. + @Test + public void evenMedianTest() { + MAFilter filter = new MAFilter(4, 0); + filter.add(1); + filter.add(3); + filter.add(4); + filter.add(2); + assertThat(filter.getResult().value).isEqualTo((2f + 3f) / 2f); + } + + // Ensures that a filter operates properly even when its window is not full. + @Test + public void shortFilterTest() { + MAFilter filter = new MAFilter(4, 0); // median + filter.add(5); + assertThat(filter.getResult().value).isEqualTo(5); + filter.add(6); + assertThat(filter.getResult().value).isEqualTo((5f + 6f) / 2); + filter.add(7); + assertThat(filter.getResult().value).isEqualTo(6); + } + + @Test + public void mixCutTest() { + MAFilter filter = new MAFilter(5, 0.5f); // Average half + filter.add(3); + filter.add(13); + filter.add(7); + filter.add(11); + filter.add(2); + assertThat(filter.getResult().value).isEqualTo((3 + 7 + 11) / 3f); + } + + @Test + public void getTest() { + MAFilter filter = new MAFilter(5, 0.5f); // Average half + assertThat(filter.getCut()).isEqualTo(0.5f); + assertThat(filter.getWindowSize()).isEqualTo(5); + } +} diff --git a/service/tests/src/com/android/server/uwb/correction/filtering/MARotationFilterTest.java b/service/tests/src/com/android/server/uwb/correction/filtering/MARotationFilterTest.java new file mode 100644 index 00000000..7e070d97 --- /dev/null +++ b/service/tests/src/com/android/server/uwb/correction/filtering/MARotationFilterTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.filtering; + +import static com.android.server.uwb.correction.TestHelpers.assertClose; +import static com.android.server.uwb.correction.math.MathHelper.F_HALF_PI; +import static com.android.server.uwb.correction.math.MathHelper.F_PI; + +import static java.lang.Math.toRadians; + +import org.junit.Test; + +public class MARotationFilterTest { + @Test + public void averageTest() { + MARotationFilter filter = new MARotationFilter(3, 1); + filter.add((float) toRadians(175)); + filter.add((float) toRadians(-175)); + filter.add((float) toRadians(5)); + + // See if this average of values on either side of 180 averages out correctly. + assertClose(filter.getResult().value, toRadians((175 + (360 - 175) + 5) / 3f)); + } + + @Test + public void remapTest() { + MARotationFilter filter = new MARotationFilter(3, 1); + filter.add((float) toRadians(175)); + filter.add((float) toRadians(-175)); + filter.add((float) toRadians(5)); + // Just like the averageTest, but now we're going to add 90 degrees, which + // should make the answer roll-over across the +/-180 boundary + filter.remap(b -> b + F_HALF_PI); + + // See if this average of values on either side of 180 averages out correctly. + assertClose( + filter.getResult().value, + toRadians((175 + (360 - 175) + 5) / 3f) + F_HALF_PI - 2 * F_PI + ); + } +} diff --git a/service/tests/src/com/android/server/uwb/correction/filtering/NullFilter.java b/service/tests/src/com/android/server/uwb/correction/filtering/NullFilter.java new file mode 100644 index 00000000..ca057153 --- /dev/null +++ b/service/tests/src/com/android/server/uwb/correction/filtering/NullFilter.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.filtering; + +import androidx.annotation.NonNull; + +import java.time.Instant; + +public class NullFilter implements IFilter { + + @NonNull + Instant mWhen = Instant.now(); + float mValue; + + /** + * Adds a value to the filter. + * + * @param value The value to add to the filter. + * @param instant When the value occurred, used to determine the latency introduced by the + * filter. Note that this has no effect on the order in which the filter operates + */ + @Override + public void add(float value, @NonNull Instant instant) { + mWhen = instant; + this.mValue = value; + } + + /** + * Alters the state of the filter such that it anticipates a change by the given amount. For + * example, if the filter is working with distance, and the distance of the next reading is + * expected to increase by 1 meter, 'shift' should be 1. + * + * @param shift How much to alter the filter state. + */ + @Override + public void compensate(float shift) { + this.mValue += shift; + } + + /** + * Gets a sample object with the result from the last computation. The sample's time is the + * average time of the samples that created the result, effectively describing the latency + * introduced by the filter. + * + * @return The result from the last computation. + */ + @NonNull + @Override + public Sample getResult() { + return new Sample(mValue, mWhen); + } +} diff --git a/service/tests/src/com/android/server/uwb/correction/math/AoAVectorTest.java b/service/tests/src/com/android/server/uwb/correction/math/AoAVectorTest.java index 5782784b..1a7854e4 100644 --- a/service/tests/src/com/android/server/uwb/correction/math/AoAVectorTest.java +++ b/service/tests/src/com/android/server/uwb/correction/math/AoAVectorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ public class AoAVectorTest { assertClose(toDegrees(vec.elevation), 10); // This is looking right and up so far that you're basically looking - // at what's behind you on your left. + // at what's behind you on your left. vec = AoAVector.fromRadians((float) toRadians(5), (float) toRadians(110), 10); assertClose(vec.azimuth, toRadians(-175)); // +5deg from "behind". @@ -129,7 +129,7 @@ public class AoAVectorTest { // looking up. AoAVector gimbalLock = AoAVector.fromCartesian(new Vector3(0, 1, 0)); // Note that this suffers from gimbal lock - meaning that ALL azimuth values are valid - // when looking up or down. + // when looking up or down. assertClose(toDegrees(gimbalLock.elevation), 90); assertClose(gimbalLock.distance, 1); diff --git a/service/tests/src/com/android/server/uwb/correction/math/MathHelperTest.java b/service/tests/src/com/android/server/uwb/correction/math/MathHelperTest.java index f66e15d3..5dcc954e 100644 --- a/service/tests/src/com/android/server/uwb/correction/math/MathHelperTest.java +++ b/service/tests/src/com/android/server/uwb/correction/math/MathHelperTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/service/tests/src/com/android/server/uwb/correction/math/QuaternionTest.java b/service/tests/src/com/android/server/uwb/correction/math/QuaternionTest.java index 6814661b..08f97345 100644 --- a/service/tests/src/com/android/server/uwb/correction/math/QuaternionTest.java +++ b/service/tests/src/com/android/server/uwb/correction/math/QuaternionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ public class QuaternionTest { assertTrue(Math.abs(ypr.z - 1.5) < 0.001); // See the Javadoc for the 'Quaternion' class for an explanation of how these - // results are determined. + // results are determined. quaternion = Quaternion.yawPitchRoll((float) Math.PI / 2, 0, 0); assertClose(quaternion.rotateVector(new Vector3(1, 2, 3)), new Vector3(3, 2, -1)); diff --git a/service/tests/src/com/android/server/uwb/correction/math/SphericalVectorTest.java b/service/tests/src/com/android/server/uwb/correction/math/SphericalVectorTest.java index 8f0a0a11..f3d112d6 100644 --- a/service/tests/src/com/android/server/uwb/correction/math/SphericalVectorTest.java +++ b/service/tests/src/com/android/server/uwb/correction/math/SphericalVectorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ public class SphericalVectorTest { assertClose(toDegrees(vec.elevation), 10); // This is looking right and up so far that you're basically looking - // at what's behind you on your left. + // at what's behind you on your left. vec = SphericalVector.fromRadians((float) toRadians(5), (float) toRadians(110), 10); assertClose(vec.azimuth, toRadians(-175)); // +5deg from "behind". @@ -116,7 +116,7 @@ public class SphericalVectorTest { // looking up. SphericalVector gimbalLock = SphericalVector.fromCartesian(new Vector3(0, 1, 0)); // Note that this suffers from gimbal lock - meaning that ALL azimuth values are valid - // when looking up or down. + // when looking up or down. assertClose(toDegrees(gimbalLock.elevation), 90); assertClose(gimbalLock.distance, 1); diff --git a/service/tests/src/com/android/server/uwb/correction/math/Vector3Tests.java b/service/tests/src/com/android/server/uwb/correction/math/Vector3Tests.java new file mode 100644 index 00000000..200b4623 --- /dev/null +++ b/service/tests/src/com/android/server/uwb/correction/math/Vector3Tests.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.uwb.correction.math; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.Presubmit; + +import org.junit.Test; + +@Presubmit +public class Vector3Tests { + + @Test + public void testNorm() { + assertThat(Vector3.ORIGIN.normalized().lengthSquared()).isEqualTo(0); + } + + @Test + public void testClamp() { + Vector3 min = new Vector3(-1, -2, -3); + Vector3 max = new Vector3(5, 6, 7); + + // Clamp x min + assertThat( + new Vector3(-7, 2, 1) + .clamp(min, max) + .subtract(new Vector3(-1, 2, 1)) + .lengthSquared() + ).isEqualTo(0); + + // Clamp y max + assertThat( + new Vector3(2, 7, 1) + .clamp(min, max) + .subtract(new Vector3(2, 6, 1)) + .lengthSquared() + ).isEqualTo(0); + + // Clamp z min + assertThat( + new Vector3(-1, 4, -9) + .clamp(min, max) + .subtract(new Vector3(-1, 4, -3)) + .lengthSquared() + ).isEqualTo(0); + } + + @Test + public void testInverted() { + Vector3 n = new Vector3(5, 10, 15); + assertThat(n.add(n.inverted()).lengthSquared()).isEqualTo(0); + } + + @Test + public void testToString() { + Vector3 v3 = new Vector3(1, -2, -13.1f); + assertThat(v3.toString()).isEqualTo("[ 1.0, -2.0,-13.1]"); + } +} diff --git a/service/tests/src/com/android/server/uwb/correction/pose/NullPoseSource.java b/service/tests/src/com/android/server/uwb/correction/pose/NullPoseSource.java new file mode 100644 index 00000000..f8950d24 --- /dev/null +++ b/service/tests/src/com/android/server/uwb/correction/pose/NullPoseSource.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.pose; + +import androidx.annotation.NonNull; + +import com.android.server.uwb.correction.math.Pose; + +import java.util.EnumSet; + +public class NullPoseSource extends PoseSourceBase { + + private EnumSet<Capabilities> mCapabilities = Capabilities.ALL; + + /** + * Gets the capabilities of this pose source. + * + * @return An EnumSet of Capabilities. + */ + @NonNull + @Override + public EnumSet<Capabilities> getCapabilities() { + return mCapabilities; + } + + /** + * Starts the pose source. Called by the {@link PoseSourceBase} when the first listener + * subscribes. + */ + @Override + protected void start() { + + } + + /** + * Stops the pose source. Called by the {@link PoseSourceBase} when the last listener + * unsubscribes. + */ + @Override + protected void stop() { + + } + + public void changePose(Pose pose) { + publish(pose); + } + + public void setCapabilities(EnumSet<Capabilities> mCapabilities) { + this.mCapabilities = mCapabilities; + } +} diff --git a/service/tests/src/com/android/server/uwb/correction/primers/AoAPrimerTest.java b/service/tests/src/com/android/server/uwb/correction/primers/AoAPrimerTest.java new file mode 100644 index 00000000..49121534 --- /dev/null +++ b/service/tests/src/com/android/server/uwb/correction/primers/AoAPrimerTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.primers; + +import static java.lang.Math.toRadians; + +import com.android.server.uwb.correction.TestHelpers; +import com.android.server.uwb.correction.math.SphericalVector; +import com.android.server.uwb.correction.math.SphericalVector.Sparse; + +import com.google.common.truth.Truth; + +import org.junit.Test; + +public class AoAPrimerTest { + @Test + public void conversionTest() { + AoAPrimer primer = new AoAPrimer(); + Sparse sv = SphericalVector.fromDegrees(35, 0, 10) + .toSparse(); + Sparse result = primer.prime(sv, null, null); + + // With zero elevation, the conversion should do nothing. + TestHelpers.assertClose(result.vector.azimuth, toRadians(35)); + + // This signal hit the azimuth antennas at an angle of 45 degrees because it came in + // at a downward angle - meaning the true spherical azimuth is 90deg. + sv = SphericalVector.fromDegrees(45, 45, 10) + .toSparse(); + + result = primer.prime(sv, null, null); + TestHelpers.assertClose(result.vector.azimuth, toRadians(90)); + TestHelpers.assertClose(result.vector.elevation, toRadians(45)); + } + + @Test + public void missingDataTest() { + // Make sure data is unchanged when there is a missing azimuth or elevation. + AoAPrimer primer = new AoAPrimer(); + SphericalVector sv = SphericalVector.fromDegrees(2, 3, 4); + + Sparse result = primer.prime(sv.toSparse(false, true, true), null, null); + Truth.assertThat(result.hasAzimuth).isFalse(); + Truth.assertThat(result.hasElevation).isTrue(); + Truth.assertThat(result.hasDistance).isTrue(); + Truth.assertThat(result.vector.elevation).isEqualTo(sv.elevation); + + result = primer.prime(sv.toSparse(true, false, true), null, null); + Truth.assertThat(result.hasAzimuth).isTrue(); + Truth.assertThat(result.hasElevation).isFalse(); + Truth.assertThat(result.hasDistance).isTrue(); + Truth.assertThat(result.vector.azimuth).isEqualTo(sv.azimuth); + } +} diff --git a/service/tests/src/com/android/server/uwb/correction/primers/ElevationPrimerTest.java b/service/tests/src/com/android/server/uwb/correction/primers/ElevationPrimerTest.java new file mode 100644 index 00000000..d6eddeb5 --- /dev/null +++ b/service/tests/src/com/android/server/uwb/correction/primers/ElevationPrimerTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.primers; + +import static com.android.server.uwb.correction.TestHelpers.assertClose; + +import static com.google.common.truth.Truth.assertThat; + +import com.android.server.uwb.correction.math.Pose; +import com.android.server.uwb.correction.math.Quaternion; +import com.android.server.uwb.correction.math.SphericalVector; +import com.android.server.uwb.correction.math.SphericalVector.Sparse; +import com.android.server.uwb.correction.math.Vector3; +import com.android.server.uwb.correction.pose.IPoseSource.Capabilities; +import com.android.server.uwb.correction.pose.NullPoseSource; + +import org.junit.Test; + +import java.util.EnumSet; + +public class ElevationPrimerTest { + @Test + public void hasElevationTest() { + ElevationPrimer primer = new ElevationPrimer(); + NullPoseSource nps = new NullPoseSource(); + nps.setCapabilities(EnumSet.of(Capabilities.UPRIGHT)); + + Sparse input = SphericalVector.fromDegrees(35, 0, 10).toSparse(); + Sparse result = primer.prime(input, null, nps); + + assertThat(result.hasElevation).isTrue(); + // Verify that, since elevation was already available, it was unchanged. + assertThat(result.vector.elevation).isEqualTo(input.vector.elevation); + } + + @Test + public void noPoseTest() { + ElevationPrimer primer = new ElevationPrimer(); + NullPoseSource nps = new NullPoseSource(); // Note: no upright capability. + + Sparse input = SphericalVector.fromDegrees(35, 0, 10) + .toSparse(true, false, true); + + Sparse result = primer.prime(input, null, nps); + // Pose is not capable of guessing elevation. + assertThat(result.hasElevation).isFalse(); + + result = primer.prime(input, null, null); + // Pose source does not exist. + assertThat(result.hasElevation).isFalse(); + } + + @Test + public void noElevationTest() { + ElevationPrimer primer = new ElevationPrimer(); + NullPoseSource nps = new NullPoseSource(); + nps.setCapabilities(EnumSet.of(Capabilities.UPRIGHT)); + float rads = (float) Math.toRadians(-5); + nps.changePose(new Pose(Vector3.ORIGIN, Quaternion.yawPitchRoll(0, rads, 0))); + + Sparse input = SphericalVector.fromDegrees(35, 0, 10) + .toSparse(true, false, true); + SphericalVector prediction = SphericalVector.fromDegrees(5, 6, 7); + Sparse result = primer.prime(input, prediction, nps); + + assertThat(result.hasElevation).isTrue(); + // The phone pose is slightly facing down, so the elevation should be slightly up. + assertClose(result.vector.elevation, -rads); + } +} diff --git a/service/tests/src/com/android/server/uwb/correction/primers/FoVPrimerTest.java b/service/tests/src/com/android/server/uwb/correction/primers/FoVPrimerTest.java new file mode 100644 index 00000000..4aabe1b6 --- /dev/null +++ b/service/tests/src/com/android/server/uwb/correction/primers/FoVPrimerTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.primers; + +import static com.google.common.truth.Truth.assertThat; + +import static java.lang.Math.toRadians; + +import com.android.server.uwb.correction.math.Quaternion; +import com.android.server.uwb.correction.math.SphericalVector; +import com.android.server.uwb.correction.math.SphericalVector.Sparse; +import com.android.server.uwb.correction.math.Vector3; + +import org.junit.Test; + +public class FoVPrimerTest { + @Test + public void conversionTest() { + FovPrimer primer = new FovPrimer((float) toRadians(45)); + Sparse input, result; + SphericalVector prediction = SphericalVector.fromDegrees(0, 0, 0); + + // The FOV formula reduced to az+el<fov, which seems too simple to be real. + // To test this, I'll place a point one degree with the FOV, and one outside the FOV, + // then "roll" and test those points in increments all the way around the perimeter. + Quaternion roll10 = Quaternion.yawPitchRoll(0, 0, (float) toRadians(10)); + Vector3 within = SphericalVector.fromDegrees(44, 0, 10).toCartesian(); + Vector3 outside = SphericalVector.fromDegrees(46, 0, 10).toCartesian(); + for (int x = 0; x < 36; x++) { + // Test within + input = SphericalVector.fromCartesian(within).toSparse(); + result = primer.prime(input, prediction, null); + assertThat(result.vector.azimuth).isEqualTo(input.vector.azimuth); + assertThat(result.vector.elevation).isEqualTo(input.vector.elevation); + within = roll10.rotateVector(within); + + // Test outside + input = SphericalVector.fromCartesian(outside).toSparse(); + result = primer.prime(input, prediction, null); + assertThat(result.vector.azimuth).isEqualTo(0); + assertThat(result.vector.elevation).isEqualTo(0); + outside = roll10.rotateVector(outside); + } + } + + @Test + public void edgeCases() { + FovPrimer primer = new FovPrimer((float) toRadians(45)); + Sparse input, result; + SphericalVector prediction = SphericalVector.fromDegrees(0, 0, 0); + + // FOV is actually permitted behind "behind" the device too, test that. + input = SphericalVector.fromDegrees(35 + 180, 1, 10).toSparse(); + result = primer.prime(input, prediction, null); + // This is within FOV. + assertThat(result.vector.azimuth).isEqualTo(input.vector.azimuth); + assertThat(result.vector.elevation).isEqualTo(input.vector.elevation); + + input = SphericalVector.fromDegrees(45 + 180, 1, 10).toSparse(); + result = primer.prime(input, prediction, null); + // This is not within FOV. + assertThat(result.vector.azimuth).isEqualTo(0); + assertThat(result.vector.elevation).isEqualTo(0); + + // Also test point at 0,0. + input = SphericalVector.fromDegrees(0, 0, 10).toSparse(); + result = primer.prime(input, prediction, null); + // This is within FOV. + assertThat(result.vector.azimuth).isEqualTo(input.vector.azimuth); + assertThat(result.vector.elevation).isEqualTo(input.vector.elevation); + + // Point at 90deg. + input = SphericalVector.fromDegrees(0, 90, 10).toSparse(); + result = primer.prime(input, prediction, null); + // This is not within FOV. + assertThat(result.vector.azimuth).isEqualTo(0); + assertThat(result.vector.elevation).isEqualTo(0); + + // Beyond maximum FOV + primer = new FovPrimer((float) toRadians(200)); + // FOV is actually permitted behind "behind" the device too, test that. + input = SphericalVector.fromDegrees(35 + 180, 1, 10).toSparse(); + result = primer.prime(input, prediction, null); + // This is within FOV. + assertThat(result.vector.azimuth).isEqualTo(input.vector.azimuth); + assertThat(result.vector.elevation).isEqualTo(input.vector.elevation); + + // No prediction + primer = new FovPrimer((float) toRadians(10)); + // Beyond the FOV, but no prediction data so it should go unchanged. + input = SphericalVector.fromDegrees(35, 1, 10).toSparse(); + result = primer.prime(input, null, null); + // This is within FOV. + assertThat(result.vector.azimuth).isEqualTo(input.vector.azimuth); + assertThat(result.vector.elevation).isEqualTo(input.vector.elevation); + } +} diff --git a/service/tests/src/com/android/server/uwb/correction/primers/NullPrimer.java b/service/tests/src/com/android/server/uwb/correction/primers/NullPrimer.java new file mode 100644 index 00000000..e6ad3fd4 --- /dev/null +++ b/service/tests/src/com/android/server/uwb/correction/primers/NullPrimer.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.correction.primers; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.server.uwb.correction.math.SphericalVector; +import com.android.server.uwb.correction.math.SphericalVector.Sparse; +import com.android.server.uwb.correction.pose.IPoseSource; + +public class NullPrimer implements IPrimer { + + /** + * Applies corrections to a raw position. + * + * @param input The original UWB reading. + * @param prediction A prediction of where the signal probably came from. + * @param poseSource A pose source that may indicate phone orientation. + * @return A replacement value for the UWB input that has been corrected for the situation. + */ + @Override + public Sparse prime(@NonNull Sparse input, @Nullable SphericalVector prediction, + @Nullable IPoseSource poseSource) { + // This test primer will just turn any negative azimuth values to positive ones, + // and use the prediction for any missing values. + float azimuth = input.vector.azimuth; + float elevation = input.vector.elevation; + float distance = input.vector.distance; + if (!input.hasAzimuth) { + azimuth = prediction.azimuth; + } + if (!input.hasElevation) { + elevation = prediction.elevation; + } + if (!input.hasDistance) { + distance = prediction.distance; + } + return SphericalVector.fromRadians(Math.abs(azimuth), elevation, distance).toSparse(); + } +} diff --git a/service/tests/src/com/android/server/uwb/params/FiraEncoderTest.java b/service/tests/src/com/android/server/uwb/params/FiraEncoderTest.java index bcc80015..7e0bb5b5 100644 --- a/service/tests/src/com/android/server/uwb/params/FiraEncoderTest.java +++ b/service/tests/src/com/android/server/uwb/params/FiraEncoderTest.java @@ -25,6 +25,7 @@ import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_TYPE_CONTROL import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_UT_TAG; 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_UL_TDOA; +import static com.google.uwb.support.fira.FiraParams.SESSION_TYPE_RANGING; import static com.google.uwb.support.fira.FiraParams.TX_TIMESTAMP_40_BIT; import static com.google.uwb.support.fira.FiraParams.UL_TDOA_DEVICE_ID_16_BIT; @@ -57,6 +58,7 @@ public class FiraEncoderTest { new FiraOpenSessionParams.Builder() .setProtocolVersion(FiraParams.PROTOCOL_VERSION_1_1) .setSessionId(1) + .setSessionType(SESSION_TYPE_RANGING) .setRangeDataNtfConfig(RANGE_DATA_NTF_CONFIG_ENABLE_PROXIMITY_AOA_LEVEL_TRIG) .setDeviceType(RANGING_DEVICE_TYPE_CONTROLLER) .setDeviceRole(RANGING_DEVICE_ROLE_RESPONDER) @@ -68,15 +70,15 @@ public class FiraEncoderTest { .setStaticStsIV(new byte[]{0x1a, 0x55, 0x77, 0x47, 0x7e, 0x7d}) .setRangeDataNtfAoaAzimuthLower(-1.5) .setRangeDataNtfAoaAzimuthUpper(2.5) - .setRangeDataNtfAoaElevationLower(-2.5) - .setRangeDataNtfAoaElevationUpper(3); + .setRangeDataNtfAoaElevationLower(-1.5) + .setRangeDataNtfAoaElevationUpper(1.2); private static final byte[] TEST_FIRA_OPEN_SESSION_TLV_DATA = UwbUtil.getByteArray("000101010101020100030100040109050101060206040702060408" + "0260090904C80000000B01000C01030D01010E01040F0200001002204E11010012010314010" + "A1501021601001701011B01191C01001F01002301002401002501322601002702780528061A" + "5577477E7D2901012A0200002C01002D01002E01012F01013101" - + "003501012B04000000001D04079E6161"); + + "003501012B04000000001D0807D59E4707D56022"); private static final FiraRangingReconfigureParams.Builder TEST_FIRA_RECONFIGURE_PARAMS = new FiraRangingReconfigureParams.Builder() @@ -86,16 +88,17 @@ public class FiraEncoderTest { .setRangeDataProximityNear(4) .setRangeDataAoaAzimuthLower(-1.5) .setRangeDataAoaAzimuthUpper(2.5) - .setRangeDataAoaElevationLower(-2.5) - .setRangeDataAoaElevationUpper(3); + .setRangeDataAoaElevationLower(-1.5) + .setRangeDataAoaElevationUpper(1.2); private static final byte[] TEST_FIRA_RECONFIGURE_TLV_DATA = - UwbUtil.getByteArray("2D01060E01040F020400100206001D04079E61F1"); + UwbUtil.getByteArray("2D01060E01040F020400100206001D0807D59E4707D56022"); private static final FiraOpenSessionParams.Builder TEST_FIRA_UT_TAG_OPEN_SESSION_PARAM = new FiraOpenSessionParams.Builder() .setProtocolVersion(FiraParams.PROTOCOL_VERSION_1_1) .setSessionId(2) + .setSessionType(SESSION_TYPE_RANGING) .setDeviceType(RANGING_DEVICE_TYPE_CONTROLLER) .setDeviceRole(RANGING_DEVICE_UT_TAG) .setDeviceAddress(UwbAddress.fromBytes(new byte[] { 0x4, 0x6})) @@ -115,12 +118,12 @@ public class FiraEncoderTest { + "0260090904C80000000B01000C01030D01010E01010F0200001002204E11010412010314010" + "A1501021601001701011B01191C01001F01002301002401002501322601002702780528061A" + "5577477E7D2901012A0200002C01002D01002E01012F01013101003501012B0400000000330" - + "8B00400000000000034081E000000000000003703010B0A380101"); + + "4B004000034041E0000003703010B0A380101"); private final FiraEncoder mFiraEncoder = new FiraEncoder(); @Test - public void testFiraOpenSesisonParams() throws Exception { + public void testFiraOpenSessionParams() throws Exception { FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build(); TlvBuffer tlvs = mFiraEncoder.getTlvBuffer(params); diff --git a/service/uci/jni/Android.bp b/service/uci/jni/Android.bp index 3a4e0141..5f188cd8 100644 --- a/service/uci/jni/Android.bp +++ b/service/uci/jni/Android.bp @@ -34,6 +34,9 @@ rust_ffi_shared { "libuci_hal_android", "libuwb_core", ], + sanitize: { + hwaddress: false, + }, } rust_test { diff --git a/service/uci/jni/src/notification_manager_android.rs b/service/uci/jni/src/notification_manager_android.rs index bd9b67c0..bed03a3b 100644 --- a/service/uci/jni/src/notification_manager_android.rs +++ b/service/uci/jni/src/notification_manager_android.rs @@ -24,6 +24,7 @@ use std::sync::Arc; use jni::objects::{GlobalRef, JClass, JMethodID, JObject, JValue}; use jni::signature::TypeSignature; +use jni::sys::jvalue; use jni::{AttachGuard, JavaVM}; use log::{debug, error}; use uwb_core::error::{Error, Result}; @@ -254,7 +255,7 @@ pub(crate) struct NotificationManagerAndroid { /// Global reference to the java class holding the various UCI notification callback functions. pub callback_obj: GlobalRef, // *_jmethod_id are cached for faster callback using call_method_unchecked - pub jmethod_id_map: HashMap<String, JMethodID<'static>>, + pub jmethod_id_map: HashMap<String, JMethodID>, // jclass are cached for faster callback pub jclass_map: HashMap<String, GlobalRef>, } @@ -275,15 +276,17 @@ impl NotificationManagerAndroid { if jclass_map.get(class_name).is_none() { // Find class using the class loader object, needed as this call is initiated from a // different native thread. + + let env_class_name = *env.new_string(class_name).map_err(|e| { + error!("UCI JNI: failed to create Java String: {e:?}"); + Error::ForeignFunctionInterface + })?; let class_value = env .call_method( class_loader_obj.as_obj(), "findClass", "(Ljava/lang/String;)Ljava/lang/Class;", - &[JValue::Object(JObject::from(env.new_string(class_name).map_err(|e| { - error!("UCI JNI: failed to create Java String: {:?}", e); - Error::ForeignFunctionInterface - })?))], + &[JValue::Object(env_class_name)], ) .map_err(|e| { error!("UCI JNI: failed to find java class {}: {:?}", class_name, e); @@ -309,7 +312,7 @@ impl NotificationManagerAndroid { Ok(jclass_map.get(class_name).unwrap().as_obj().into()) } - fn cached_jni_call(&mut self, name: &str, sig: &str, args: &[JValue]) -> Result<()> { + fn cached_jni_call(&mut self, name: &str, sig: &str, args: &[jvalue]) -> Result<()> { debug!("UCI JNI: callback {}", name); let type_signature = TypeSignature::from_str(sig).map_err(|e| { error!("UCI JNI: Invalid type signature: {:?}", e); @@ -357,9 +360,9 @@ impl NotificationManagerAndroid { "onSessionStatusNotificationReceived", "(JII)V", &[ - JValue::Long(session_id as i64), - JValue::Int(session_state as i32), - JValue::Int(reason_code as i32), + jvalue::from(JValue::Long(session_id as i64)), + jvalue::from(JValue::Int(session_state as i32)), + jvalue::from(JValue::Int(reason_code as i32)), ], ) } @@ -400,6 +403,16 @@ impl NotificationManagerAndroid { MULTICAST_LIST_UPDATE_STATUS_CLASS, )?; let method_sig = "(L".to_owned() + MULTICAST_LIST_UPDATE_STATUS_CLASS + ";)V"; + + // Safety: mac_address_jintarray is safely instantiated above. + let mac_address_jobject = unsafe { JObject::from_raw(mac_address_jintarray) }; + + // Safety: subsession_id_jlongarray is safely instantiated above. + let subsession_id_jobject = unsafe { JObject::from_raw(subsession_id_jlongarray) }; + + // Safety: status_jintarray is safely instantiated above. + let status_jobject = unsafe { JObject::from_raw(status_jintarray) }; + let multicast_update_jobject = self .env .new_object( @@ -409,16 +422,16 @@ impl NotificationManagerAndroid { JValue::Long(session_id as i64), JValue::Int(remaining_multicast_list_size), JValue::Int(count), - JValue::Object(JObject::from(mac_address_jintarray)), - JValue::Object(JObject::from(subsession_id_jlongarray)), - JValue::Object(JObject::from(status_jintarray)), + JValue::Object(mac_address_jobject), + JValue::Object(subsession_id_jobject), + JValue::Object(status_jobject), ], ) .map_err(|_| Error::ForeignFunctionInterface)?; self.cached_jni_call( "onMulticastListUpdateNotificationReceived", &method_sig, - &[JValue::Object(multicast_update_jobject)], + &[jvalue::from(JValue::Object(multicast_update_jobject))], ) } @@ -443,8 +456,10 @@ impl NotificationManagerAndroid { uwb_core::uci::RangingMeasurements::ExtendedAddressTwoWay(_) => { EXTENDED_MAC_ADDRESS_LEN } - uwb_core::uci::RangingMeasurements::ShortDltdoa(_) => SHORT_MAC_ADDRESS_LEN, - uwb_core::uci::RangingMeasurements::ExtendedDltdoa(_) => EXTENDED_MAC_ADDRESS_LEN, + uwb_core::uci::RangingMeasurements::ShortAddressDltdoa(_) => SHORT_MAC_ADDRESS_LEN, + uwb_core::uci::RangingMeasurements::ExtendedAddressDltdoa(_) => { + EXTENDED_MAC_ADDRESS_LEN + } _ => { return Err(Error::ForeignFunctionInterface); } @@ -459,13 +474,21 @@ impl NotificationManagerAndroid { .env .new_byte_array(MAX_RANGING_ROUNDS_LEN) .map_err(|_| Error::ForeignFunctionInterface)?; + + // Safety: address_jbytearray is safely instantiated above. + let address_jobject = unsafe { JObject::from_raw(address_jbytearray) }; + // Safety: anchor_location is safely instantiated above. + let anchor_jobject = unsafe { JObject::from_raw(anchor_location) }; + // Safety: active_ranging_rounds is safely instantiated above. + let active_ranging_rounds_jobject = unsafe { JObject::from_raw(active_ranging_rounds) }; + let zero_initiated_measurement_jobject = self .env .new_object( measurement_jclass, "([BIIIIIIIIIIIJJIIJJI[B[B)V", &[ - JValue::Object(JObject::from(address_jbytearray)), + JValue::Object(address_jobject), JValue::Int(0), JValue::Int(0), JValue::Int(0), @@ -484,8 +507,8 @@ impl NotificationManagerAndroid { JValue::Long(0), JValue::Long(0), JValue::Int(0), - JValue::Object(JObject::from(anchor_location)), - JValue::Object(JObject::from(active_ranging_rounds)), + JValue::Object(anchor_jobject), + JValue::Object(active_ranging_rounds_jobject), ], ) .map_err(|e| { @@ -495,8 +518,8 @@ impl NotificationManagerAndroid { let measurement_count: i32 = match &range_data.ranging_measurements { RangingMeasurements::ShortAddressTwoWay(v) => v.len(), RangingMeasurements::ExtendedAddressTwoWay(v) => v.len(), - RangingMeasurements::ShortDltdoa(v) => v.len(), - RangingMeasurements::ExtendedDltdoa(v) => v.len(), + RangingMeasurements::ShortAddressDltdoa(v) => v.len(), + RangingMeasurements::ExtendedAddressDltdoa(v) => v.len(), _ => { return Err(Error::BadParameters); } @@ -506,8 +529,8 @@ impl NotificationManagerAndroid { let mac_indicator = match &range_data.ranging_measurements { RangingMeasurements::ShortAddressTwoWay(_) => MacAddressIndicator::ShortAddress, RangingMeasurements::ExtendedAddressTwoWay(_) => MacAddressIndicator::ExtendedAddress, - RangingMeasurements::ShortDltdoa(_) => MacAddressIndicator::ShortAddress, - RangingMeasurements::ExtendedDltdoa(_) => MacAddressIndicator::ExtendedAddress, + RangingMeasurements::ShortAddressDltdoa(_) => MacAddressIndicator::ShortAddress, + RangingMeasurements::ExtendedAddressDltdoa(_) => MacAddressIndicator::ExtendedAddress, _ => { return Err(Error::BadParameters); } @@ -523,10 +546,10 @@ impl NotificationManagerAndroid { .map_err(|_| Error::ForeignFunctionInterface)?; for (i, measurement) in match range_data.ranging_measurements { - RangingMeasurements::ShortDltdoa(v) => { + RangingMeasurements::ShortAddressDltdoa(v) => { v.into_iter().map(DlTdoaRangingMeasurement::from).collect::<Vec<_>>() } - RangingMeasurements::ExtendedDltdoa(v) => { + RangingMeasurements::ExtendedAddressDltdoa(v) => { v.into_iter().map(DlTdoaRangingMeasurement::from).collect::<Vec<_>>() } _ => Vec::new(), @@ -558,13 +581,22 @@ impl NotificationManagerAndroid { .env .byte_array_from_slice(&measurement.ranging_rounds) .map_err(|_| Error::ForeignFunctionInterface)?; + + // Safety: mac_address_jbytearray is safely instantiated above. + let mac_address_jobject = unsafe { JObject::from_raw(mac_address_jbytearray) }; + // Safety: dt_anchor_location_jbytearray is safely instantiated above. + let dt_anchor_location_jobject = + unsafe { JObject::from_raw(dt_anchor_location_jbytearray) }; + // Safety: ranging_rounds_jbytearray is safely instantiated above. + let ranging_rounds_jobject = unsafe { JObject::from_raw(ranging_rounds_jbytearray) }; + let measurement_jobject = self .env .new_object( measurement_jclass, "([BIIIIIIIIIIIJJIIJJI[B[B)V", &[ - JValue::Object(JObject::from(mac_address_jbytearray)), + JValue::Object(mac_address_jobject), JValue::Int(measurement.status as i32), JValue::Int(measurement.message_type as i32), JValue::Int(measurement.message_control as i32), @@ -583,8 +615,8 @@ impl NotificationManagerAndroid { JValue::Long(measurement.initiator_reply_time as i64), JValue::Long(measurement.responder_reply_time as i64), JValue::Int(measurement.initiator_responder_tof as i32), - JValue::Object(JObject::from(dt_anchor_location_jbytearray)), - JValue::Object(JObject::from(ranging_rounds_jbytearray)), + JValue::Object(dt_anchor_location_jobject), + JValue::Object(ranging_rounds_jobject), ], ) .map_err(|e| { @@ -607,6 +639,12 @@ impl NotificationManagerAndroid { )?; let method_sig = "(JJIJIII[L".to_owned() + UWB_DL_TDOA_MEASUREMENT_CLASS + ";[B)V"; + + // Safety: measurements_jobjectarray is safely instantiated above. + let measurements_jobject = unsafe { JObject::from_raw(measurements_jobjectarray) }; + // Safety: raw_notification_jbytearray is safely instantiated above. + let raw_notification_jobject = unsafe { JObject::from_raw(raw_notification_jbytearray) }; + let range_data_jobject = self .env .new_object( @@ -620,19 +658,20 @@ impl NotificationManagerAndroid { JValue::Int(range_data.ranging_measurement_type as i32), JValue::Int(mac_indicator as i32), JValue::Int(measurement_count), - JValue::Object(JObject::from(measurements_jobjectarray)), - JValue::Object(JObject::from(raw_notification_jbytearray)), + JValue::Object(measurements_jobject), + JValue::Object(raw_notification_jobject), ], ) .map_err(|e| { error!("UCI JNI: Ranging Data object creation failed: {:?}", e); Error::ForeignFunctionInterface })?; + let method_sig = "(L".to_owned() + UWB_RANGING_DATA_CLASS + ";)V"; self.cached_jni_call( "onRangeDataNotificationReceived", &method_sig, - &[JValue::Object(range_data_jobject)], + &[jvalue::from(JValue::Object(range_data_jobject))], ) } @@ -650,13 +689,17 @@ impl NotificationManagerAndroid { )?; let address_jbytearray = self.env.new_byte_array(bytearray_len).map_err(|_| Error::ForeignFunctionInterface)?; + + // Safety: address_jbytearray is safely instantiated above. + let address_jobject = unsafe { JObject::from_raw(address_jbytearray) }; + let zero_initiated_measurement_jobject = self .env .new_object( measurement_jclass, "([BIIIIIIIIIIIII)V", &[ - JValue::Object(JObject::from(address_jbytearray)), + JValue::Object(address_jobject), JValue::Int(0), JValue::Int(0), JValue::Int(0), @@ -676,6 +719,7 @@ impl NotificationManagerAndroid { error!("UCI JNI: measurement object creation failed: {:?}", e); Error::ForeignFunctionInterface })?; + let measurements_jobjectarray = self .env .new_object_array( @@ -700,13 +744,16 @@ impl NotificationManagerAndroid { .set_byte_array_region(mac_address_jbytearray, 0, &mac_address_i8) .map_err(|_| Error::ForeignFunctionInterface)?; // casting as i32 is fine since it is wider than actual integer type. + + // Safety: mac_address_jbytearray is safely instantiated above. + let mac_address_jobject = unsafe { JObject::from_raw(mac_address_jbytearray) }; let measurement_jobject = self .env .new_object( measurement_jclass, "([BIIIIIIIIIIIII)V", &[ - JValue::Object(JObject::from(mac_address_jbytearray)), + JValue::Object(mac_address_jobject), JValue::Int(measurement.status as i32), JValue::Int(measurement.nlos as i32), JValue::Int(measurement.distance as i32), @@ -751,13 +798,16 @@ impl NotificationManagerAndroid { )?; let address_jbytearray = self.env.new_byte_array(bytearray_len).map_err(|_| Error::ForeignFunctionInterface)?; + + // Safety: address_jbytearray is safely instantiated above. + let address_jobject = unsafe { JObject::from_raw(address_jbytearray) }; let zero_initiated_measurement_jobject = self .env .new_object( measurement_jclass, "([BIIIIIIII)V", &[ - JValue::Object(JObject::from(address_jbytearray)), + JValue::Object(address_jobject), JValue::Int(0), JValue::Int(0), JValue::Int(0), @@ -797,13 +847,16 @@ impl NotificationManagerAndroid { .set_byte_array_region(mac_address_jbytearray, 0, &mac_address_i8) .map_err(|_| Error::ForeignFunctionInterface)?; // casting as i32 is fine since it is wider than actual integer type. + + // Safety: mac_address_jbytearray is safely instantiated above. + let mac_address_jobject = unsafe { JObject::from_raw(mac_address_jbytearray) }; let measurement_jobject = self .env .new_object( measurement_jclass, "([BIIIIIIII)V", &[ - JValue::Object(JObject::from(mac_address_jbytearray)), + JValue::Object(mac_address_jobject), JValue::Int(measurement.status as i32), JValue::Int(measurement.nlos as i32), JValue::Int(measurement.frame_sequence_number as i32), @@ -840,12 +893,12 @@ impl NotificationManagerAndroid { let (bytearray_len, mac_indicator) = match &range_data.ranging_measurements { RangingMeasurements::ExtendedAddressTwoWay(_) | RangingMeasurements::ExtendedAddressOwrAoa(_) - | RangingMeasurements::ExtendedDltdoa(_) => { + | RangingMeasurements::ExtendedAddressDltdoa(_) => { (EXTENDED_MAC_ADDRESS_LEN, MacAddressIndicator::ExtendedAddress) } RangingMeasurements::ShortAddressTwoWay(_) | RangingMeasurements::ShortAddressOwrAoa(_) - | RangingMeasurements::ShortDltdoa(_) => { + | RangingMeasurements::ShortAddressDltdoa(_) => { (SHORT_MAC_ADDRESS_LEN, MacAddressIndicator::ShortAddress) } }; @@ -854,15 +907,14 @@ impl NotificationManagerAndroid { | RangingMeasurements::ShortAddressTwoWay(_) => UWB_TWO_WAY_MEASUREMENT_CLASS, RangingMeasurements::ExtendedAddressOwrAoa(_) | RangingMeasurements::ShortAddressOwrAoa(_) => UWB_OWR_AOA_MEASUREMENT_CLASS, - RangingMeasurements::ExtendedDltdoa(_) | RangingMeasurements::ShortDltdoa(_) => { - UWB_DL_TDOA_MEASUREMENT_CLASS - } + RangingMeasurements::ExtendedAddressDltdoa(_) + | RangingMeasurements::ShortAddressDltdoa(_) => UWB_DL_TDOA_MEASUREMENT_CLASS, }; let measurement_count: i32 = match &range_data.ranging_measurements { RangingMeasurements::ShortAddressTwoWay(v) => v.len().try_into(), RangingMeasurements::ExtendedAddressTwoWay(v) => v.len().try_into(), - RangingMeasurements::ShortDltdoa(v) => v.len().try_into(), - RangingMeasurements::ExtendedDltdoa(v) => v.len().try_into(), + RangingMeasurements::ShortAddressDltdoa(v) => v.len().try_into(), + RangingMeasurements::ExtendedAddressDltdoa(v) => v.len().try_into(), RangingMeasurements::ShortAddressOwrAoa(v) => v.len().try_into(), RangingMeasurements::ExtendedAddressOwrAoa(v) => v.len().try_into(), } @@ -915,6 +967,11 @@ impl NotificationManagerAndroid { UWB_RANGING_DATA_CLASS, )?; let method_sig = "(JJIJIII[L".to_owned() + measurement_data_class + ";[B)V"; + + // Safety: measurements_jobjectarray is safely instantiated above. + let measurements_jobject = unsafe { JObject::from_raw(measurements_jobjectarray) }; + // Safety: raw_notification_jobject is safely instantiated above. + let raw_notification_jobject = unsafe { JObject::from_raw(raw_notification_jbytearray) }; let range_data_jobject = self .env .new_object( @@ -928,8 +985,8 @@ impl NotificationManagerAndroid { JValue::Int(range_data.ranging_measurement_type as i32), JValue::Int(mac_indicator as i32), JValue::Int(measurement_count), - JValue::Object(JObject::from(measurements_jobjectarray)), - JValue::Object(JObject::from(raw_notification_jbytearray)), + JValue::Object(measurements_jobject), + JValue::Object(raw_notification_jobject), ], ) .map_err(|e| { @@ -940,7 +997,7 @@ impl NotificationManagerAndroid { self.cached_jni_call( "onRangeDataNotificationReceived", &method_sig, - &[JValue::Object(range_data_jobject)], + &[jvalue::from(JValue::Object(range_data_jobject))], ) } } @@ -948,21 +1005,24 @@ impl NotificationManagerAndroid { impl NotificationManager for NotificationManagerAndroid { fn on_core_notification(&mut self, core_notification: CoreNotification) -> Result<()> { debug!("UCI JNI: core notification callback."); + + let env_chip_id_jobject = *self.env.new_string(&self.chip_id).unwrap(); + match core_notification { CoreNotification::DeviceStatus(device_state) => self.cached_jni_call( "onDeviceStatusNotificationReceived", "(ILjava/lang/String;)V", &[ - JValue::Int(device_state as i32), - JValue::Object(JObject::from(self.env.new_string(&self.chip_id).unwrap())), + jvalue::from(JValue::Int(device_state as i32)), + jvalue::from(JValue::Object(env_chip_id_jobject)), ], ), CoreNotification::GenericError(generic_error) => self.cached_jni_call( "onCoreGenericErrorNotificationReceived", "(ILjava/lang/String;)V", &[ - JValue::Int(generic_error as i32), - JValue::Object(JObject::from(self.env.new_string(&self.chip_id).unwrap())), + jvalue::from(JValue::Int(generic_error as i32)), + jvalue::from(JValue::Object(env_chip_id_jobject)), ], ), } @@ -986,7 +1046,7 @@ impl NotificationManager for NotificationManagerAndroid { // TODO(b/246678053): Refactor to do this split inside // on_session_range_data_notification(), after computing the common parameters based on // the RangingMeasurements type. - SessionNotification::RangeData(range_data) => match range_data.ranging_measurements { + SessionNotification::SessionInfo(range_data) => match range_data.ranging_measurements { uwb_core::uci::RangingMeasurements::ShortAddressTwoWay(_) => { self.on_session_range_data_notification(range_data) } @@ -999,13 +1059,31 @@ impl NotificationManager for NotificationManagerAndroid { uwb_core::uci::RangingMeasurements::ExtendedAddressOwrAoa(_) => { self.on_session_range_data_notification(range_data) } - uwb_core::uci::RangingMeasurements::ShortDltdoa(_) => { + uwb_core::uci::RangingMeasurements::ShortAddressDltdoa(_) => { self.on_session_dl_tdoa_range_data_notification(range_data) } - uwb_core::uci::RangingMeasurements::ExtendedDltdoa(_) => { + uwb_core::uci::RangingMeasurements::ExtendedAddressDltdoa(_) => { self.on_session_dl_tdoa_range_data_notification(range_data) } }, + // These session notifications should not come here, as they are handled within + // UciManager, for internal state management related to sending data packet(s). + SessionNotification::DataCredit { session_id, credit_availability } => { + error!( + "UCI JNI: Received unexpected DataCredit notification for \ + session_id {}, credit_availability {}", + session_id, credit_availability + ); + Ok(()) + } + SessionNotification::DataTransferStatus { session_id, uci_sequence_number, status } => { + error!( + "UCI JNI: Received unexpected DataTransferStatus notification for \ + session_id {}, uci_sequence_number {} with status {}", + session_id, uci_sequence_number, status + ); + Ok(()) + } } } @@ -1018,14 +1096,21 @@ impl NotificationManager for NotificationManagerAndroid { .env .byte_array_from_slice(&vendor_notification.payload) .map_err(|_| Error::ForeignFunctionInterface)?; + + // Safety: payload_jbytearray safely instantiated above. + let payload_jobject = unsafe { JObject::from_raw(payload_jbytearray) }; self.cached_jni_call( "onVendorUciNotificationReceived", "(II[B)V", &[ // Java only has signed integer. The range for signed int32 should be sufficient. - JValue::Int(vendor_notification.gid.try_into().map_err(|_| Error::BadParameters)?), - JValue::Int(vendor_notification.oid.try_into().map_err(|_| Error::BadParameters)?), - JValue::Object(JObject::from(payload_jbytearray)), + jvalue::from(JValue::Int( + vendor_notification.gid.try_into().map_err(|_| Error::BadParameters)?, + )), + jvalue::from(JValue::Int( + vendor_notification.oid.try_into().map_err(|_| Error::BadParameters)?, + )), + jvalue::from(JValue::Object(payload_jobject)), ], ) } @@ -1048,17 +1133,22 @@ impl NotificationManager for NotificationManagerAndroid { .env .byte_array_from_slice(&data_rcv_notification.payload) .map_err(|_| Error::ForeignFunctionInterface)?; + + // Safety: source_address_jbytearray safely instantiated above. + let source_address_jobject = unsafe { JObject::from_raw(source_address_jbytearray) }; + // Safety: payload_jbytearray safely instantiated above. + let payload_jobject = unsafe { JObject::from_raw(payload_jbytearray) }; self.cached_jni_call( "onDataReceived", "(JIJ[BII[B)V", &[ - JValue::Long(data_rcv_notification.session_id as i64), - JValue::Int(data_rcv_notification.status as i32), - JValue::Long(data_rcv_notification.uci_sequence_num as i64), - JValue::Object(JObject::from(source_address_jbytearray)), - JValue::Int(data_rcv_notification.source_fira_component as i32), - JValue::Int(data_rcv_notification.dest_fira_component as i32), - JValue::Object(JObject::from(payload_jbytearray)), + jvalue::from(JValue::Long(data_rcv_notification.session_id as i64)), + jvalue::from(JValue::Int(data_rcv_notification.status as i32)), + jvalue::from(JValue::Long(data_rcv_notification.uci_sequence_num as i64)), + jvalue::from(JValue::Object(source_address_jobject)), + jvalue::from(JValue::Int(data_rcv_notification.source_fira_component as i32)), + jvalue::from(JValue::Int(data_rcv_notification.dest_fira_component as i32)), + jvalue::from(JValue::Object(payload_jobject)), ], ) } diff --git a/service/uci/jni/src/uci_jni_android_new.rs b/service/uci/jni/src/uci_jni_android_new.rs index 3be8268b..a2552692 100644 --- a/service/uci/jni/src/uci_jni_android_new.rs +++ b/service/uci/jni/src/uci_jni_android_new.rs @@ -27,9 +27,9 @@ use std::iter::zip; use jni::errors::Error as JNIError; use jni::objects::{GlobalRef, JObject, JString, JValue}; -use jni::signature::JavaType; +use jni::signature::ReturnType; use jni::sys::{ - jboolean, jbyte, jbyteArray, jint, jintArray, jlong, jobject, jobjectArray, jshortArray, + jboolean, jbyte, jbyteArray, jint, jintArray, jlong, jobject, jobjectArray, jshortArray, jvalue, }; use jni::JNIEnv; use log::{debug, error}; @@ -41,8 +41,8 @@ use uwb_core::params::{ }; use uwb_uci_packets::{ AppConfigTlvType, CapTlv, Controlee, Controlee_V2_0_16_Byte_Version, - Controlee_V2_0_32_Byte_Version, Controlees, PowerStats, ResetConfig, SessionState, SessionType, - StatusCode, UpdateMulticastListAction, + Controlee_V2_0_32_Byte_Version, Controlees, FiraComponent, PowerStats, ResetConfig, + SessionState, SessionType, StatusCode, UpdateMulticastListAction, }; /// Macro capturing the name of the function calling this macro. @@ -330,6 +330,9 @@ fn create_set_config_response(response: SetAppConfigResponse, env: JNIEnv) -> Re } let config_status_jbytearray = env.byte_array_from_slice(&buf).map_err(|_| Error::ForeignFunctionInterface)?; + + // Safety: config_status_jbytearray is safely instantiated above. + let config_status_jobject = unsafe { JObject::from_raw(config_status_jbytearray) }; let config_status_jobject = env .new_object( uwb_config_status_class, @@ -337,7 +340,7 @@ fn create_set_config_response(response: SetAppConfigResponse, env: JNIEnv) -> Re &[ JValue::Int(response.status as i32), JValue::Int(response.config_status.len() as i32), - JValue::Object(JObject::from(config_status_jbytearray)), + JValue::Object(config_status_jobject), ], ) .map_err(|_| Error::ForeignFunctionInterface)?; @@ -405,18 +408,21 @@ fn create_get_config_response(tlvs: Vec<AppConfigTlv>, env: JNIEnv) -> Result<jb } let tlvs_jbytearray = env.byte_array_from_slice(&buf).map_err(|_| Error::ForeignFunctionInterface)?; - let tlvs_jobject = env + + // Safety: tlvs_jbytearray is safely instantiated above. + let tlvs_jobject = unsafe { JObject::from_raw(tlvs_jbytearray) }; + let tlvs_jobject_env = env .new_object( tlv_data_class, "(II[B)V", &[ JValue::Int(StatusCode::UciStatusOk as i32), JValue::Int(tlvs_len as i32), - JValue::Object(JObject::from(tlvs_jbytearray)), + JValue::Object(tlvs_jobject), ], ) .map_err(|_| Error::ForeignFunctionInterface)?; - Ok(*tlvs_jobject) + Ok(*tlvs_jobject_env) } /// Get app configurations on a single UWB device. Return null JObject if failed. @@ -477,18 +483,21 @@ fn create_cap_response(tlvs: Vec<CapTlv>, env: JNIEnv) -> Result<jbyteArray> { } let tlvs_jbytearray = env.byte_array_from_slice(&buf).map_err(|_| Error::ForeignFunctionInterface)?; - let tlvs_jobject = env + + // Safety: tlvs_jbytearray is safely instantiated above. + let tlvs_jobject = unsafe { JObject::from_raw(tlvs_jbytearray) }; + let tlvs_jobject_env = env .new_object( tlv_data_class, "(II[B)V", &[ JValue::Int(StatusCode::UciStatusOk as i32), JValue::Int(tlvs.len() as i32), - JValue::Object(JObject::from(tlvs_jbytearray)), + JValue::Object(tlvs_jobject), ], ) .map_err(|_| Error::ForeignFunctionInterface)?; - Ok(*tlvs_jobject) + Ok(*tlvs_jobject_env) } /// Get capability info on a single UWB device. Return null JObject if failed. @@ -582,8 +591,6 @@ fn native_controller_multicast_list_update( { return Err(Error::BadParameters); } - let sub_session_key_list = - env.convert_byte_array(sub_session_keys).map_err(|_| Error::ForeignFunctionInterface)?; let controlee_list = match UpdateMulticastListAction::from_u8(action as u8) .ok_or(Error::BadParameters)? { @@ -596,27 +603,37 @@ fn native_controller_multicast_list_update( } UpdateMulticastListAction::AddControleeWithShortSubSessionKey => { Controlees::ShortSessionKey( - zip(zip(address_list, sub_session_id_list), sub_session_key_list.chunks(16)) - .map(|((address, id), key)| { - Ok(Controlee_V2_0_16_Byte_Version { - short_address: address as u16, - subsession_id: id as u32, - subsession_key: key.try_into().map_err(|_| Error::BadParameters)?, - }) - }) - .collect::<Result<Vec<Controlee_V2_0_16_Byte_Version>>>()?, - ) - } - UpdateMulticastListAction::AddControleeWithLongSubSessionKey => Controlees::LongSessionKey( - zip(zip(address_list, sub_session_id_list), sub_session_key_list.chunks(32)) + zip( + zip(address_list, sub_session_id_list), + env.convert_byte_array(sub_session_keys) + .map_err(|_| Error::ForeignFunctionInterface)? + .chunks(16), + ) .map(|((address, id), key)| { - Ok(Controlee_V2_0_32_Byte_Version { + Ok(Controlee_V2_0_16_Byte_Version { short_address: address as u16, subsession_id: id as u32, subsession_key: key.try_into().map_err(|_| Error::BadParameters)?, }) }) - .collect::<Result<Vec<Controlee_V2_0_32_Byte_Version>>>()?, + .collect::<Result<Vec<Controlee_V2_0_16_Byte_Version>>>()?, + ) + } + UpdateMulticastListAction::AddControleeWithLongSubSessionKey => Controlees::LongSessionKey( + zip( + zip(address_list, sub_session_id_list), + env.convert_byte_array(sub_session_keys) + .map_err(|_| Error::ForeignFunctionInterface)? + .chunks(32), + ) + .map(|((address, id), key)| { + Ok(Controlee_V2_0_32_Byte_Version { + short_address: address as u16, + subsession_id: id as u32, + subsession_key: key.try_into().map_err(|_| Error::BadParameters)?, + }) + }) + .collect::<Result<Vec<Controlee_V2_0_32_Byte_Version>>>()?, ), }; uci_manager.session_update_controller_multicast_list( @@ -677,9 +694,18 @@ fn native_set_log_mode(env: JNIEnv, obj: JObject, log_mode_jstring: JString) -> dispatcher.set_logger_mode(logger_mode) } -fn create_vendor_response(msg: RawUciMessage, env: JNIEnv) -> Result<jobject> { +// # Safety +// +// For this to be safe, the validity of msg should be checked before calling. +unsafe fn create_vendor_response(msg: RawUciMessage, env: JNIEnv) -> Result<jobject> { let vendor_response_class = env.find_class(VENDOR_RESPONSE_CLASS).map_err(|_| Error::ForeignFunctionInterface)?; + + // Unsafe from_raw call + let payload_jobject = JObject::from_raw( + env.byte_array_from_slice(&msg.payload).map_err(|_| Error::ForeignFunctionInterface)?, + ); + match env.new_object( vendor_response_class, "(BII[B)V", @@ -687,10 +713,7 @@ fn create_vendor_response(msg: RawUciMessage, env: JNIEnv) -> Result<jobject> { JValue::Byte(StatusCode::UciStatusOk as i8), JValue::Int(msg.gid as i32), JValue::Int(msg.oid as i32), - JValue::Object(JObject::from( - env.byte_array_from_slice(&msg.payload) - .map_err(|_| Error::ForeignFunctionInterface)?, - )), + JValue::Object(payload_jobject), ], ) { Ok(obj) => Ok(*obj), @@ -716,7 +739,10 @@ fn create_invalid_vendor_response(env: JNIEnv) -> Result<jobject> { } } -fn create_ranging_round_status( +/// Safety: +/// +/// response should be checked before calling to ensure safety. +unsafe fn create_ranging_round_status( response: SessionUpdateActiveRoundsDtTagResponse, env: JNIEnv, ) -> Result<jobject> { @@ -724,16 +750,19 @@ fn create_ranging_round_status( .find_class(DT_RANGING_ROUNDS_STATUS_CLASS) .map_err(|_| Error::ForeignFunctionInterface)?; let indexes = response.ranging_round_indexes; + + // Unsafe from_raw call + let indexes_jobject = JObject::from_raw( + env.byte_array_from_slice(indexes.as_ref()).map_err(|_| Error::ForeignFunctionInterface)?, + ); + match env.new_object( dt_ranging_rounds_update_status_class, "(II[B)V", &[ JValue::Int(response.status as i32), JValue::Int(indexes.len() as i32), - JValue::Object(JObject::from( - env.byte_array_from_slice(indexes.as_ref()) - .map_err(|_| Error::ForeignFunctionInterface)?, - )), + JValue::Object(indexes_jobject), ], ) { Ok(o) => Ok(*o), @@ -759,12 +788,17 @@ pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeSe ) { // Note: unwrap() here is not desirable, but unavoidable given non-null object is returned // even for failing cases. - Some(msg) => create_vendor_response(msg, env) - .map_err(|e| { - error!("{} failed with {:?}", function_name!(), &e); - e - }) - .unwrap_or_else(|_| create_invalid_vendor_response(env).unwrap()), + + // Safety: create_vendor_response is unsafe, however msg is safely returned from + // native_send_raw_vendor_cmd. + Some(msg) => unsafe { + create_vendor_response(msg, env) + .map_err(|e| { + error!("{} failed with {:?}", function_name!(), &e); + e + }) + .unwrap_or_else(|_| create_invalid_vendor_response(env).unwrap()) + }, None => create_invalid_vendor_response(env).unwrap(), } } @@ -847,12 +881,15 @@ pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeSe ), function_name!(), ) { - Some(rr) => create_ranging_round_status(rr, env) - .map_err(|e| { - error!("{} failed with {:?}", function_name!(), &e); - e - }) - .unwrap_or(*JObject::null()), + // Safety: rr is safely returned from native_set_ranging_rounds_dt_tag + Some(rr) => unsafe { + create_ranging_round_status(rr, env) + .map_err(|e| { + error!("{} failed with {:?}", function_name!(), &e); + e + }) + .unwrap_or(*JObject::null()) + }, None => *JObject::null(), } } @@ -871,6 +908,62 @@ fn native_set_ranging_rounds_dt_tag( uci_manager.session_update_active_rounds_dt_tag(session_id, indexes) } +/// Send a data packet to the remote device. +#[no_mangle] +pub extern "system" fn Java_com_android_server_uwb_jni_NativeUwbManager_nativeSendData( + env: JNIEnv, + obj: JObject, + session_id: jint, + address: jbyteArray, + dest_fira_component: jbyte, + uci_sequence_number: jbyte, + app_payload_data: jbyteArray, + chip_id: JString, +) -> jbyte { + debug!("{}: enter", function_name!()); + byte_result_helper( + native_send_data( + env, + obj, + session_id, + address, + dest_fira_component, + uci_sequence_number, + app_payload_data, + chip_id, + ), + function_name!(), + ) +} + +#[allow(clippy::too_many_arguments)] +fn native_send_data( + env: JNIEnv, + obj: JObject, + session_id: jint, + address: jbyteArray, + dest_fira_component: jbyte, + uci_sequence_number: jbyte, + app_payload_data: jbyteArray, + chip_id: JString, +) -> Result<()> { + let uci_manager = Dispatcher::get_uci_manager(env, obj, chip_id) + .map_err(|_| Error::ForeignFunctionInterface)?; + let address_bytearray = + env.convert_byte_array(address).map_err(|_| Error::ForeignFunctionInterface)?; + let app_payload_data_bytearray = + env.convert_byte_array(app_payload_data).map_err(|_| Error::ForeignFunctionInterface)?; + let destination_fira_component = + FiraComponent::from_u8(dest_fira_component as u8).ok_or(Error::BadParameters)?; + uci_manager.send_data_packet( + session_id as u32, + address_bytearray, + destination_fira_component, + uci_sequence_number as u8, + app_payload_data_bytearray, + ) +} + /// Get the class loader object. Has to be called from a JNIEnv where the local java classes are /// loaded. Results in a global reference to the class loader object that can be used to look for /// classes in other native thread. @@ -886,8 +979,8 @@ fn get_class_loader_obj(env: &JNIEnv) -> Result<GlobalRef> { .call_method_unchecked( ranging_data_class, get_class_loader_method, - JavaType::Object("java/lang/ClassLoader".into()), - &[JValue::Void], + ReturnType::Object, + &[jvalue::from(JValue::Void)], ) .map_err(|_| Error::ForeignFunctionInterface)?; let class_loader_jobject = class_loader.l().map_err(|_| Error::ForeignFunctionInterface)?; diff --git a/tests/cts/hostsidetests/multidevices/uwb/lib/uwb_ranging_params.py b/tests/cts/hostsidetests/multidevices/uwb/lib/uwb_ranging_params.py index 0d5a333a..e7939b8c 100644 --- a/tests/cts/hostsidetests/multidevices/uwb/lib/uwb_ranging_params.py +++ b/tests/cts/hostsidetests/multidevices/uwb/lib/uwb_ranging_params.py @@ -72,6 +72,9 @@ class FiraParamEnums: MULTICAST_LIST_UPDATE_ACTION_ADD = 0 MULTICAST_LIST_UPDATE_ACTION_DELETE = 1 + # sts config + STS_CONFIG_STATIC = 0 + @dataclasses.dataclass class UwbRangingReconfigureParams(): @@ -127,6 +130,7 @@ class UwbRangingParams(): multi_node_mode: Ranging mode. Possible values 1 to 1 or 1 to many. vendor_id: Ranging device vendor ID. static_sts_iv: Static STS value. + sts_config: STS config. Example: An example of UWB ranging parameters passed to sl4a is below. @@ -176,6 +180,7 @@ class UwbRangingParams(): vendor_id: List[int] = dataclasses.field(default_factory=lambda: [5, 6]) static_sts_iv: List[int] = dataclasses.field( default_factory=lambda: [5, 6, 7, 8, 9, 10]) + sts_config: int = FiraParamEnums.STS_CONFIG_STATIC def to_dict(self) -> Dict[str, Any]: """Returns UWB ranging parameters in dictionary for sl4a. @@ -205,6 +210,7 @@ class UwbRangingParams(): "multiNodeMode": self.multi_node_mode, "vendorId": self.vendor_id, "staticStsIV": self.static_sts_iv, + "stsConfig": self.sts_config, } def update(self, **kwargs: Any): diff --git a/tests/cts/hostsidetests/multidevices/uwb/ranging_test.py b/tests/cts/hostsidetests/multidevices/uwb/ranging_test.py index da0596e3..7c46360d 100644 --- a/tests/cts/hostsidetests/multidevices/uwb/ranging_test.py +++ b/tests/cts/hostsidetests/multidevices/uwb/ranging_test.py @@ -83,8 +83,7 @@ class RangingTest(uwb_base_test.UwbBaseTest): self.responder.close_ranging() self.initiator.close_ranging() - def teardown_class(self): - super().teardown_class() + def on_fail(self, record): for count, ad in enumerate(self.android_devices): test_name = "initiator" if not count else "responder" ad.take_bug_report( diff --git a/tests/cts/hostsidetests/multidevices/uwb/snippet/UwbManagerSnippet.java b/tests/cts/hostsidetests/multidevices/uwb/snippet/UwbManagerSnippet.java index 0926e615..b58fa04e 100644 --- a/tests/cts/hostsidetests/multidevices/uwb/snippet/UwbManagerSnippet.java +++ b/tests/cts/hostsidetests/multidevices/uwb/snippet/UwbManagerSnippet.java @@ -452,15 +452,23 @@ public class UwbManagerSnippet implements Snippet { if (j.has("preamble")) { builder.setPreambleCodeIndex(j.getInt("preamble")); } - if (j.has("vendorId")) { - JSONArray jArray = j.getJSONArray("vendorId"); - byte[] bArray = convertJSONArrayToByteArray(jArray); - builder.setVendorId(bArray); - } - if (j.has("staticStsIV")) { - JSONArray jArray = j.getJSONArray("staticStsIV"); - byte[] bArray = convertJSONArrayToByteArray(jArray); - builder.setStaticStsIV(bArray); + if (j.getInt("stsConfig") == FiraParams.STS_CONFIG_STATIC) { + JSONArray jVendorIdArray = j.getJSONArray("vendorId"); + builder.setVendorId(convertJSONArrayToByteArray(jVendorIdArray)); + JSONArray jStatisStsIVArray = j.getJSONArray("staticStsIV"); + builder.setStaticStsIV(convertJSONArrayToByteArray(jStatisStsIVArray)); + } else if (j.getInt("stsConfig") == FiraParams.STS_CONFIG_PROVISIONED) { + builder.setStsConfig(j.getInt("stsConfig")); + JSONArray jSessionKeyArray = j.getJSONArray("sessionKey"); + builder.setSessionKey(convertJSONArrayToByteArray(jSessionKeyArray)); + } else if (j.getInt( + "stsConfig") == FiraParams.STS_CONFIG_PROVISIONED_FOR_CONTROLEE_INDIVIDUAL_KEY) { + builder.setStsConfig(j.getInt("stsConfig")); + JSONArray jSessionKeyArray = j.getJSONArray("sessionKey"); + builder.setSessionKey(convertJSONArrayToByteArray(jSessionKeyArray)); + JSONArray jSubSessionKeyArray = j.getJSONArray("subSessionKey"); + builder.setSubsessionKey(convertJSONArrayToByteArray(jSubSessionKeyArray)); + builder.setSubSessionId(j.getInt("subSessionId")); } if (j.has("aoaResultRequest")) { builder.setAoaResultRequest(j.getInt("aoaResultRequest")); diff --git a/tests/cts/hostsidetests/multidevices/uwb/uwb_manager_test.py b/tests/cts/hostsidetests/multidevices/uwb/uwb_manager_test.py index a107d988..70b0e50b 100644 --- a/tests/cts/hostsidetests/multidevices/uwb/uwb_manager_test.py +++ b/tests/cts/hostsidetests/multidevices/uwb/uwb_manager_test.py @@ -45,8 +45,7 @@ class UwbManagerTest(uwb_base_test.UwbBaseTest): super().setup_class() self.dut = self.android_devices[0] - def teardown_class(self): - super().teardown_class() + def on_fail(self, record): self.dut.take_bug_report(destination=self.current_test_info.output_path) ### Helper methods ### diff --git a/tests/cts/tests/Android.bp b/tests/cts/tests/Android.bp index 306606fb..81b7e70a 100644 --- a/tests/cts/tests/Android.bp +++ b/tests/cts/tests/Android.bp @@ -40,4 +40,5 @@ android_test { ], srcs: ["src/**/*.java"], platform_apis: true, + min_sdk_version: "31", } diff --git a/tests/cts/tests/src/android/uwb/cts/UwbManagerTest.java b/tests/cts/tests/src/android/uwb/cts/UwbManagerTest.java index 731d8944..44df61bf 100644 --- a/tests/cts/tests/src/android/uwb/cts/UwbManagerTest.java +++ b/tests/cts/tests/src/android/uwb/cts/UwbManagerTest.java @@ -871,6 +871,7 @@ public class UwbManagerTest { FiraOpenSessionParams firaOpenSessionParams = new FiraOpenSessionParams.Builder() .setProtocolVersion(new FiraProtocolVersion(1, 1)) .setSessionId(1) + .setSessionType(FiraParams.SESSION_TYPE_RANGING) .setStsConfig(FiraParams.STS_CONFIG_STATIC) .setVendorId(new byte[]{0x5, 0x6}) .setStaticStsIV(new byte[]{0x5, 0x6, 0x9, 0xa, 0x4, 0x6}) |