diff options
23 files changed, 1713 insertions, 206 deletions
@@ -1,3 +1,7 @@ rpius@google.com steveliu@google.com zning@google.com + +# For the rust codebase +per-file *.rs = akahuang@google.com +per-file *.bp = akahuang@google.com
\ No newline at end of file diff --git a/service/Android.bp b/service/Android.bp index 3b154b6c..bcc3301c 100644 --- a/service/Android.bp +++ b/service/Android.bp @@ -64,7 +64,7 @@ java_library { static_libs: [ "androidx.annotation_annotation", - "android.hardware.uwb.fira_android-V1-java", + "android.hardware.uwb.fira_android-V2-java", "com.uwb.support.ccc", "com.uwb.support.fira", "com.uwb.support.generic", diff --git a/service/java/com/android/server/uwb/UwbInjector.java b/service/java/com/android/server/uwb/UwbInjector.java index c99c3ce2..11453a78 100644 --- a/service/java/com/android/server/uwb/UwbInjector.java +++ b/service/java/com/android/server/uwb/UwbInjector.java @@ -32,6 +32,7 @@ import android.os.Binder; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; +import android.os.Process; import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; @@ -330,6 +331,12 @@ public class UwbInjector { return false; } + /** Whether the uid is signed with the same key as the platform. */ + public boolean isAppSignedWithPlatformKey(int uid) { + return mContext.getPackageManager().checkSignatures(uid, Process.SYSTEM_UID) + == PackageManager.SIGNATURE_MATCH; + } + /** Helper method to retrieve app importance. */ private int getPackageImportance(int uid, @NonNull String packageName) { try { diff --git a/service/java/com/android/server/uwb/UwbServiceCore.java b/service/java/com/android/server/uwb/UwbServiceCore.java index 667eb94e..aa94185e 100644 --- a/service/java/com/android/server/uwb/UwbServiceCore.java +++ b/service/java/com/android/server/uwb/UwbServiceCore.java @@ -39,9 +39,9 @@ import android.uwb.SessionHandle; import android.uwb.StateChangeReason; import android.uwb.UwbManager.AdapterStateCallback; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.uwb.data.UwbUciConstants; import com.android.server.uwb.data.UwbVendorUciResponse; import com.android.server.uwb.jni.INativeUwbManager; @@ -76,8 +76,12 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, INativeUwbManager.VendorNotification, UwbCountryCode.CountryCodeChangedListener { private static final String TAG = "UwbServiceCore"; - private static final int TASK_ENABLE = 1; - private static final int TASK_DISABLE = 2; + @VisibleForTesting + public static final int TASK_ENABLE = 1; + @VisibleForTesting + public static final int TASK_DISABLE = 2; + @VisibleForTesting + public static final int TASK_RESTART = 3; private static final int WATCHDOG_MS = 10000; private static final int SEND_VENDOR_CMD_TIMEOUT_MS = 10000; @@ -181,10 +185,10 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, // If error status is received, toggle UWB off to reset stack state. // TODO(b/227488208): Should we try to restart (like wifi) instead? if ((byte) deviceState == UwbUciConstants.DEVICE_STATE_ERROR) { - Log.e(TAG, "Error device status received. Disabling..."); + Log.e(TAG, "Error device status received. Restarting..."); mUwbMetrics.incrementDeviceStatusErrorCount(); - takBugReportAfterDeviceError("UWB is disabled due to device status error"); - setEnabled(false); + takBugReportAfterDeviceError("Restarting UWB due to vendor error"); + mEnableDisableTask.execute(TASK_RESTART); return; } handleDeviceStatusNotification(deviceState); @@ -267,30 +271,6 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, return mNativeUwbManager.getTimestampResolutionNanos(); } - /** - * Check the attribution source chain to ensure that there are no 3p apps which are not in fg - * which can receive the ranging results. - * @return true if there is some non-system app which is in not in fg, false otherwise. - */ - private boolean hasAnyNonSystemAppNotInFgInAttributionSource( - @NonNull AttributionSource attributionSource) { - // Iterate attribution source chain to ensure that there is no non-fg 3p app in the - // request. - while (attributionSource != null) { - int uid = attributionSource.getUid(); - String packageName = attributionSource.getPackageName(); - if (!mUwbInjector.isSystemApp(uid, packageName)) { - if (!mUwbInjector.isForegroundAppOrService(uid, packageName)) { - Log.e(TAG, "Found a non fg app/service in the attribution source of request: " - + attributionSource); - return true; - } - } - attributionSource = attributionSource.getNext(); - } - return false; - } - public void openRanging( AttributionSource attributionSource, SessionHandle sessionHandle, @@ -299,12 +279,6 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, if (!isUwbEnabled()) { throw new IllegalStateException("Uwb is not enabled"); } - if (hasAnyNonSystemAppNotInFgInAttributionSource(attributionSource)) { - Log.e(TAG, "openRanging - System policy disallows"); - rangingCallbacks.onRangingOpenFailed(sessionHandle, - RangingChangeReason.SYSTEM_POLICY, new PersistableBundle()); - return; - } int sessionId = 0; if (FiraParams.isCorrectProtocol(params)) { FiraOpenSessionParams firaOpenSessionParams = FiraOpenSessionParams.fromBundle( @@ -479,6 +453,13 @@ public class UwbServiceCore implements INativeUwbManager.DeviceNotification, mSessionManager.deinitAllSession(); disableInternal(); break; + + case TASK_RESTART: + mSessionManager.deinitAllSession(); + disableInternal(); + enableInternal(); + break; + default: Log.d(TAG, "EnableDisableTask : Undefined Task"); break; diff --git a/service/java/com/android/server/uwb/UwbSessionManager.java b/service/java/com/android/server/uwb/UwbSessionManager.java index 76bae008..bd05f9f8 100644 --- a/service/java/com/android/server/uwb/UwbSessionManager.java +++ b/service/java/com/android/server/uwb/UwbSessionManager.java @@ -28,6 +28,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.os.PersistableBundle; import android.os.RemoteException; import android.util.Log; import android.util.Pair; @@ -53,6 +54,7 @@ import com.google.uwb.support.ccc.CccOpenRangingParams; import com.google.uwb.support.ccc.CccParams; import com.google.uwb.support.ccc.CccRangingStartedParams; import com.google.uwb.support.ccc.CccStartRangingParams; +import com.google.uwb.support.fira.FiraOpenSessionParams; import com.google.uwb.support.fira.FiraParams; import com.google.uwb.support.fira.FiraRangingReconfigureParams; @@ -73,12 +75,18 @@ import java.util.concurrent.TimeoutException; public class UwbSessionManager implements INativeUwbManager.SessionNotification { private static final String TAG = "UwbSessionManager"; - private static final int SESSION_OPEN_RANGING = 1; - private static final int SESSION_START_RANGING = 2; - private static final int SESSION_STOP_RANGING = 3; - private static final int SESSION_RECONFIG_RANGING = 4; - private static final int SESSION_CLOSE = 5; - private static final int SESSION_ON_DEINIT = 6; + @VisibleForTesting + public static final int SESSION_OPEN_RANGING = 1; + @VisibleForTesting + public static final int SESSION_START_RANGING = 2; + @VisibleForTesting + public static final int SESSION_STOP_RANGING = 3; + @VisibleForTesting + public static final int SESSION_RECONFIG_RANGING = 4; + @VisibleForTesting + public static final int SESSION_CLOSE = 5; + @VisibleForTesting + public static final int SESSION_ON_DEINIT = 6; // TODO: don't expose the internal field for testing. @VisibleForTesting @@ -211,6 +219,19 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification Log.i(TAG, "initSession() : Enter - sessionId : " + sessionId); UwbSession uwbSession = createUwbSession(attributionSource, sessionHandle, sessionId, protocolName, params, rangingCallbacks); + // 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 = + uwbSession.hasAnyNonPrivilegedAppInAttributionSource(); + if (nonPrivilegedAppAttrSource != null && !mUwbInjector.isForegroundAppOrService( + nonPrivilegedAppAttrSource.getUid(), nonPrivilegedAppAttrSource.getPackageName())) { + Log.e(TAG, "Found a non fg 3p app/service in the attribution source of request: " + + nonPrivilegedAppAttrSource); + Log.e(TAG, "openRanging - System policy disallows for non fg 3p apps"); + rangingCallbacks.onRangingOpenFailed(sessionHandle, + RangingChangeReason.SYSTEM_POLICY, new PersistableBundle()); + return; + } if (isExistedSession(sessionId)) { Log.i(TAG, "Duplicated sessionId"); rangingCallbacks.onRangingOpenFailed(sessionHandle, RangingChangeReason.BAD_PARAMETERS, @@ -430,12 +451,22 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification } public int reconfigure(SessionHandle sessionHandle, @Nullable Params params) { - Log.i(TAG, "reconfigure() - Session Handle : " + sessionHandle); int status = UwbUciConstants.STATUS_CODE_ERROR_SESSION_NOT_EXIST; if (!isExistedSession(sessionHandle)) { Log.i(TAG, "Not initialized session ID"); return status; } + int sessionId = getSessionId(sessionHandle); + Log.i(TAG, "reconfigure() - Session ID : " + sessionId); + UwbSession uwbSession = getUwbSession(sessionId); + if (uwbSession.getProtocolName().equals(FiraParams.PROTOCOL_NAME) + && params instanceof FiraRangingReconfigureParams) { + FiraRangingReconfigureParams rangingReconfigureParams = + (FiraRangingReconfigureParams) params; + Log.i(TAG, "reconfigure() - update reconfigure params: " + + rangingReconfigureParams); + uwbSession.updateFiraParamsOnReconfigure(rangingReconfigureParams); + } Pair<SessionHandle, Params> info = new Pair<>(sessionHandle, params); mEventTask.execute(SESSION_RECONFIG_RANGING, info); return 0; @@ -892,6 +923,31 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification this.mProfileType = convertProtolNameToProfileType(protocolName); } + private boolean isPrivilegedApp(int uid, String packageName) { + return mUwbInjector.isSystemApp(uid, packageName) + || mUwbInjector.isAppSignedWithPlatformKey(uid); + } + + /** + * Check the attribution source chain to check if there are any 3p apps. + * @return true if there is some non-system app, false otherwise. + */ + @Nullable + public AttributionSource hasAnyNonPrivilegedAppInAttributionSource() { + // Iterate attribution source chain to ensure that there is no non-fg 3p app in the + // request. + AttributionSource attributionSource = mAttributionSource; + while (attributionSource != null) { + int uid = attributionSource.getUid(); + String packageName = attributionSource.getPackageName(); + if (!isPrivilegedApp(uid, packageName)) { + return attributionSource; + } + attributionSource = attributionSource.getNext(); + } + return null; + } + public AttributionSource getAttributionSource() { return this.mAttributionSource; } @@ -910,25 +966,35 @@ public class UwbSessionManager implements INativeUwbManager.SessionNotification public void updateCccParamsOnStart(CccStartRangingParams rangingStartParams) { // Need to update the RAN multiplier from the CccStartRangingParams for CCC session. - CccOpenRangingParams rangingOpenedParams = (CccOpenRangingParams) mParams; CccOpenRangingParams newParams = - new CccOpenRangingParams.Builder() - .setProtocolVersion(rangingOpenedParams.getProtocolVersion()) - .setUwbConfig(rangingOpenedParams.getUwbConfig()) - .setPulseShapeCombo(rangingOpenedParams.getPulseShapeCombo()) - .setSessionId(rangingOpenedParams.getSessionId()) + new CccOpenRangingParams.Builder((CccOpenRangingParams) mParams) .setRanMultiplier(rangingStartParams.getRanMultiplier()) - .setChannel(rangingOpenedParams.getChannel()) - .setNumChapsPerSlot(rangingOpenedParams.getNumChapsPerSlot()) - .setNumResponderNodes(rangingOpenedParams.getNumResponderNodes()) - .setNumSlotsPerRound(rangingOpenedParams.getNumSlotsPerRound()) - .setSyncCodeIndex(rangingOpenedParams.getSyncCodeIndex()) - .setHoppingConfigMode(rangingOpenedParams.getHoppingConfigMode()) - .setHoppingSequence(rangingOpenedParams.getHoppingSequence()) .build(); this.mParams = newParams; } + public void updateFiraParamsOnReconfigure(FiraRangingReconfigureParams reconfigureParams) { + // Need to update the reconfigure params from the FiraRangingReconfigureParams for + // FiRa session. + FiraOpenSessionParams.Builder newParamsBuilder = + new FiraOpenSessionParams.Builder((FiraOpenSessionParams) mParams); + if (reconfigureParams.getBlockStrideLength() != null) { + newParamsBuilder.setBlockStrideLength(reconfigureParams.getBlockStrideLength()); + } + if (reconfigureParams.getRangeDataNtfConfig() != null) { + newParamsBuilder.setRangeDataNtfConfig(reconfigureParams.getRangeDataNtfConfig()); + } + if (reconfigureParams.getRangeDataProximityNear() != null) { + newParamsBuilder.setRangeDataNtfProximityNear( + reconfigureParams.getRangeDataProximityNear()); + } + if (reconfigureParams.getRangeDataProximityFar() != null) { + newParamsBuilder.setRangeDataNtfProximityFar( + reconfigureParams.getRangeDataProximityFar()); + } + this.mParams = newParamsBuilder.build(); + } + public String getProtocolName() { return this.mProtocolName; } diff --git a/service/java/com/android/server/uwb/config/CapabilityParam.java b/service/java/com/android/server/uwb/config/CapabilityParam.java index a86ee9a4..f7d5a20e 100644 --- a/service/java/com/android/server/uwb/config/CapabilityParam.java +++ b/service/java/com/android/server/uwb/config/CapabilityParam.java @@ -43,6 +43,8 @@ public class CapabilityParam { public static final int SUPPORTED_EXTENDED_MAC_ADDRESS = 0x11; public static final int SUPPORTED_AOA_RESULT_REQ_INTERLEAVING = UwbVendorCapabilityTlvTypes.SUPPORTED_AOA_RESULT_REQ_ANTENNA_INTERLEAVING; + public static final int SUPPORTED_MIN_RANGING_INTERVAL_MS = + UwbVendorCapabilityTlvTypes.SUPPORTED_MIN_RANGING_INTERVAL_MS; // CCC specific public static final int CCC_SUPPORTED_VERSIONS = diff --git a/service/java/com/android/server/uwb/discovery/TransportServerProvider.java b/service/java/com/android/server/uwb/discovery/TransportServerProvider.java new file mode 100644 index 00000000..2c071167 --- /dev/null +++ b/service/java/com/android/server/uwb/discovery/TransportServerProvider.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.discovery; + +import androidx.annotation.WorkerThread; + +import com.android.server.uwb.discovery.info.FiraConnectorCapabilities; +import com.android.server.uwb.discovery.info.FiraConnectorMessage; + +/** Abstract class for Transport Server Provider */ +@WorkerThread +public abstract class TransportServerProvider { + + /** Callback for listening to transport server events. */ + @WorkerThread + public interface TransportServerCallback { + + /** Called when the server started processing. */ + void onProcessingStarted(); + + /** Called when the server stopped processing. */ + void onProcessingStopped(); + + /** + * Called when the server receive new capabilites from the remote device. + * + * @param capabilities new capabilities. + */ + void onCapabilitesUpdated(FiraConnectorCapabilities capabilities); + + /** + * Called when the server receive a new FiRa connector message from the remote device. + * + * @param secid destination SECID on this device. + * @param message FiRa connector message. + */ + void onMessage(int secid, FiraConnectorMessage message); + } + + /* Indicates whether the server has started. + */ + protected boolean mStarted = false; + + /** + * Checks if the server has started. + * + * @return indicates if the server has started. + */ + public boolean isStarted() { + return mStarted; + } + + /** + * Starts the transport server. + * + * @return indicates if succeefully started. + */ + public abstract boolean start(); + + /** + * Stops the transport server. + * + * @return indicates if succeefully stopped. + */ + public abstract boolean stop(); + + /** + * Send a FiRa connector message to the remote device through the transport server. + * + * @param secid destination SECID on remote device. + * @param message message to be send. + * @return indicates if succeefully started. + */ + public abstract boolean sendMessage(int secid, FiraConnectorMessage message); +} diff --git a/service/java/com/android/server/uwb/discovery/TransportServerService.java b/service/java/com/android/server/uwb/discovery/TransportServerService.java new file mode 100644 index 00000000..8f8093ab --- /dev/null +++ b/service/java/com/android/server/uwb/discovery/TransportServerService.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.discovery; + +import android.content.AttributionSource; +import android.content.Context; +import android.util.Log; + +import androidx.annotation.WorkerThread; + +import com.android.server.uwb.discovery.TransportServerProvider.TransportServerCallback; +import com.android.server.uwb.discovery.ble.GattTransportServerProvider; +import com.android.server.uwb.discovery.info.DiscoveryInfo; + +/** This service manages the TransportServerProvider. */ +@WorkerThread +public class TransportServerService { + private static final String TAG = TransportServerService.class.getSimpleName(); + + private final TransportServerProvider mTransportServerProvider; + + public TransportServerService( + AttributionSource attributionSource, + Context context, + DiscoveryInfo discoveryInfo, + TransportServerCallback transportServerCallback) + throws AssertionError { + + switch (discoveryInfo.transportType) { + case BLE: + mTransportServerProvider = + new GattTransportServerProvider( + attributionSource, context, transportServerCallback); + break; + default: + throw new AssertionError( + "Failed to create TransportServerProvider due to invalid transport type:" + + " " + + discoveryInfo.transportType); + } + } + + /** + * Start the transport server + * + * @return indicates if succeefully started. + */ + public boolean start() { + if (mTransportServerProvider.isStarted()) { + Log.i(TAG, "Transport server already started."); + return false; + } + return mTransportServerProvider.start(); + } + + /** + * Stop the transport server + * + * @return indicates if succeefully stopped. + */ + public boolean stop() { + if (!mTransportServerProvider.isStarted()) { + Log.i(TAG, "Transport server already stopped."); + return false; + } + return mTransportServerProvider.stop(); + } +} diff --git a/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryAdvertiseProvider.java b/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryAdvertiseProvider.java index 092b7d32..f70f04c1 100644 --- a/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryAdvertiseProvider.java +++ b/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryAdvertiseProvider.java @@ -170,7 +170,7 @@ public class BleDiscoveryAdvertiseProvider extends DiscoveryAdvertiseProvider { return new AdvertiseData.Builder() .setIncludeDeviceName(false) .setIncludeTxPowerLevel(false) - .addServiceUuid(DiscoveryAdvertisement.FIRA_CP_PARCEL_UUID) + .addServiceUuid(UuidConstants.FIRA_CP_PARCEL_UUID) .build(); } @@ -179,9 +179,9 @@ public class BleDiscoveryAdvertiseProvider extends DiscoveryAdvertiseProvider { new AdvertiseData.Builder() .setIncludeDeviceName(false) .setIncludeTxPowerLevel(false) - .addServiceUuid(DiscoveryAdvertisement.FIRA_CP_PARCEL_UUID) + .addServiceUuid(UuidConstants.FIRA_CP_PARCEL_UUID) .addServiceData( - DiscoveryAdvertisement.FIRA_CP_PARCEL_UUID, + UuidConstants.FIRA_CP_PARCEL_UUID, DiscoveryAdvertisement.toBytes( mAdvertiseInfo.discoveryAdvertisement, /*includeVendorSpecificData=*/ false)); diff --git a/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryScanProvider.java b/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryScanProvider.java index f3591fc4..f7758a2d 100644 --- a/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryScanProvider.java +++ b/service/java/com/android/server/uwb/discovery/ble/BleDiscoveryScanProvider.java @@ -140,7 +140,7 @@ public class BleDiscoveryScanProvider extends DiscoveryScanProvider { return; } - byte[] serviceData = record.getServiceData(DiscoveryAdvertisement.FIRA_CP_PARCEL_UUID); + byte[] serviceData = record.getServiceData(UuidConstants.FIRA_CP_PARCEL_UUID); if (serviceData == null) { Log.w(TAG, "Ignoring scan result. Empty ServiceData"); return; @@ -183,9 +183,7 @@ public class BleDiscoveryScanProvider extends DiscoveryScanProvider { } // Add scan filter for FiRa Connector Primary Service UUID. scanFilterList.add( - new ScanFilter.Builder() - .setServiceUuid(DiscoveryAdvertisement.FIRA_CP_PARCEL_UUID) - .build()); + new ScanFilter.Builder().setServiceUuid(UuidConstants.FIRA_CP_PARCEL_UUID).build()); return scanFilterList; } diff --git a/service/java/com/android/server/uwb/discovery/ble/DiscoveryAdvertisement.java b/service/java/com/android/server/uwb/discovery/ble/DiscoveryAdvertisement.java index a832666d..ce848f0b 100644 --- a/service/java/com/android/server/uwb/discovery/ble/DiscoveryAdvertisement.java +++ b/service/java/com/android/server/uwb/discovery/ble/DiscoveryAdvertisement.java @@ -17,8 +17,6 @@ package com.android.server.uwb.discovery.ble; import android.annotation.NonNull; import android.annotation.Nullable; -import android.bluetooth.BluetoothUuid; -import android.os.ParcelUuid; import android.util.Log; import android.util.SparseArray; @@ -43,14 +41,6 @@ import java.util.Optional; public class DiscoveryAdvertisement { private static final String LOG_TAG = DiscoveryAdvertisement.class.getSimpleName(); - // The FiRa service UUID for connector primary and connector secondary as defined in Bluetooth - // Specification Supplement v10. Little endian encoding. - public static final byte[] FIRA_CP_UUID = new byte[] {(byte) 0xF3, (byte) 0xFF}; - public static final byte[] FIRA_CS_UUID = new byte[] {(byte) 0xF4, (byte) 0xFF}; - - public static final ParcelUuid FIRA_CP_PARCEL_UUID = BluetoothUuid.parseUuidFrom(FIRA_CP_UUID); - public static final ParcelUuid FIRA_CS_PARCEL_UUID = BluetoothUuid.parseUuidFrom(FIRA_CS_UUID); - // Mask and value of the FiRa specific field type field within each AD field. private static final byte FIRA_SPECIFIC_FIELD_TYPE_MASK = (byte) 0xF0; private static final byte FIRA_SPECIFIC_FIELD_TYPE_UWB_INDICATION_DATA = 0x1; diff --git a/service/java/com/android/server/uwb/discovery/ble/GattTransportServerProvider.java b/service/java/com/android/server/uwb/discovery/ble/GattTransportServerProvider.java new file mode 100644 index 00000000..050e3010 --- /dev/null +++ b/service/java/com/android/server/uwb/discovery/ble/GattTransportServerProvider.java @@ -0,0 +1,494 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.discovery.ble; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattServer; +import android.bluetooth.BluetoothGattServerCallback; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.content.AttributionSource; +import android.content.Context; +import android.content.ContextParams; +import android.util.Log; + +import androidx.annotation.WorkerThread; + +import com.android.server.uwb.discovery.TransportServerProvider; +import com.android.server.uwb.discovery.TransportServerProvider.TransportServerCallback; +import com.android.server.uwb.discovery.info.FiraConnectorCapabilities; +import com.android.server.uwb.discovery.info.FiraConnectorDataPacket; +import com.android.server.uwb.discovery.info.FiraConnectorMessage; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Arrays; + +/** + * Class for UWB transport server provider using Bluetooth GATT. + * + * <p>The GATT server simply waits for the discovery from client side. It shall also wait for at + * least one valid update of FiRa Connector Capabilities characteristic value from the client side. + * Until this happens and until the client enables the Handle Value Notification method on the "OUT" + * Control Point characteristic (through Client Characteristic Configuration Descriptor), the server + * shall ignore all commands sent by Write methods through the "IN" Control Point characteristic. + */ +@WorkerThread +public class GattTransportServerProvider extends TransportServerProvider { + private static final String TAG = GattTransportServerProvider.class.getSimpleName(); + + private TransportServerCallback mTransportServerCallback; + private BluetoothManager mBluetoothManager; + private BluetoothGattServer mBluetoothGattServer; + private BluetoothDevice mRemoteGattDevice; + private FiraConnectorCapabilities mRemoteCapabilities; + private boolean mConnected; + private boolean mNotificationEnabled; + + private BluetoothGattService mFiraCPService = + new BluetoothGattService( + UuidConstants.FIRA_CP_PARCEL_UUID.getUuid(), + BluetoothGattService.SERVICE_TYPE_PRIMARY); + + private BluetoothGattCharacteristic mInControlPointCharacteristic; + private BluetoothGattCharacteristic mOutControlPointCharacteristic; + private BluetoothGattCharacteristic mCapabilitiesCharacteristic; + + private BluetoothGattDescriptor mOutControlPointCccdDescriptor; + + /* Queue of Fira Connector Data Packets from the mInControlPointCharacteristic that are + * incomplete to be constructed as FiRa Connector Message. + */ + private ArrayDeque<FiraConnectorDataPacket> mIncompleteInDataPacketQueue; + + /* Wraps Fira Connector Message byte array and the associated SECID. + */ + private static class MessagePacket { + public final int secid; + public ByteBuffer messageBytes; + + MessagePacket(int secid, ByteBuffer messageBytes) { + this.secid = secid; + this.messageBytes = messageBytes; + } + } + + /* Queue of Fira Connector Message wrapped as MessagePacket to be sent via the + * mOutControlPointCharacteristic. + */ + private ArrayDeque<MessagePacket> mOutMessageQueue; + + /** + * GATT server callbacks responsible for servicing read and write calls from the remote device + */ + private BluetoothGattServerCallback mBluetoothGattServerCallback = + new BluetoothGattServerCallback() { + @Override + public void onConnectionStateChange( + BluetoothDevice device, int status, int newState) { + Log.i(TAG, "onConnectionStateChange state:" + newState + " Device:" + device); + if (newState == BluetoothProfile.STATE_DISCONNECTED) { + mConnected = false; + startProcessing(device); + } else if (newState == BluetoothProfile.STATE_CONNECTED) { + mConnected = true; + startProcessing(device); + } + } + + @Override + public void onCharacteristicReadRequest( + BluetoothDevice device, + int requestId, + int offset, + BluetoothGattCharacteristic characteristic) { + Log.d(TAG, "onCharacteristicReadRequest"); + if (characteristic.getUuid().equals(mOutControlPointCharacteristic.getUuid())) { + Log.d(TAG, "onRead OutControlPointCharacteristic"); + mBluetoothGattServer.sendResponse( + device, + requestId, + BluetoothGatt.GATT_SUCCESS, + offset, + mOutControlPointCharacteristic.getValue()); + processOutDataPacket(); + } else { + Log.w(TAG, "onRead unknown " + characteristic.getUuid()); + } + } + + @Override + public void onCharacteristicWriteRequest( + BluetoothDevice device, + int requestId, + BluetoothGattCharacteristic characteristic, + boolean preparedWrite, + boolean responseNeeded, + int offset, + byte[] value) { + Log.d( + TAG, + "onCharacteristicWriteRequest uuid:" + + characteristic.getUuid() + + ", Length: " + + value.length); + if (characteristic.getUuid().equals(mCapabilitiesCharacteristic.getUuid())) { + Log.i(TAG, "onWrite CapabilitiesCharacteristic"); + mRemoteCapabilities = FiraConnectorCapabilities.fromBytes(value); + + if (mRemoteCapabilities != null) { + mTransportServerCallback.onCapabilitesUpdated(mRemoteCapabilities); + startProcessing(device); + + if (responseNeeded) { + mBluetoothGattServer.sendResponse( + device, + requestId, + BluetoothGatt.GATT_SUCCESS, + offset, + value); + } + return; + } + if (responseNeeded) { + mBluetoothGattServer.sendResponse( + device, + requestId, + BluetoothGatt.GATT_FAILURE, + offset, + /*value=*/ null); + } + } else if (characteristic + .getUuid() + .equals(mInControlPointCharacteristic.getUuid())) { + Log.d(TAG, "onWrite InControlPointCharacteristic"); + + boolean success = processInDataPacket(value); + + if (responseNeeded) { + if (success) { + mBluetoothGattServer.sendResponse( + device, + requestId, + BluetoothGatt.GATT_SUCCESS, + offset, + value); + } else { + mBluetoothGattServer.sendResponse( + device, + requestId, + BluetoothGatt.GATT_FAILURE, + offset, + /*value=*/ null); + } + } + } else { + Log.w(TAG, "onWrite unknown " + characteristic.getUuid()); + } + } + + @Override + public void onDescriptorWriteRequest( + BluetoothDevice device, + int requestId, + BluetoothGattDescriptor descriptor, + boolean preparedWrite, + boolean responseNeeded, + int offset, + byte[] value) { + Log.d( + TAG, + "onDescriptorWriteRequest uuid:" + + descriptor.getUuid() + + ", Length: " + + value.length); + if (descriptor.getUuid().equals(mOutControlPointCccdDescriptor.getUuid())) { + if (Arrays.equals( + BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, value)) { + Log.d(TAG, "Enable OutControlPoint value notifications: " + device); + mNotificationEnabled = true; + startProcessing(device); + } else if (Arrays.equals( + BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE, value)) { + Log.d(TAG, "Disable OutControlPoint value notifications: " + device); + mNotificationEnabled = false; + startProcessing(device); + } + if (responseNeeded) { + mBluetoothGattServer.sendResponse( + device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value); + } + } else { + Log.w(TAG, "onDescriptorWrite unknown " + descriptor.getUuid()); + if (responseNeeded) { + mBluetoothGattServer.sendResponse( + device, + requestId, + BluetoothGatt.GATT_FAILURE, + offset, + /*value=*/ null); + } + } + } + }; + + public GattTransportServerProvider( + AttributionSource attributionSource, + Context context, + TransportServerCallback transportServerCallback) { + Context attributedContext = + context.createContext( + new ContextParams.Builder() + .setNextAttributionSource(attributionSource) + .build()); + mTransportServerCallback = transportServerCallback; + mBluetoothManager = attributedContext.getSystemService(BluetoothManager.class); + mBluetoothGattServer = + mBluetoothManager.openGattServer(attributedContext, mBluetoothGattServerCallback); + + mIncompleteInDataPacketQueue = new ArrayDeque(); + mOutMessageQueue = new ArrayDeque(); + + setupGattCharacteristic(); + } + + @Override + public boolean start() { + if (mBluetoothGattServer == null) { + Log.w(TAG, "start failed due to mBluetoothGattServer is null."); + return false; + } + boolean succeed = mBluetoothGattServer.addService(mFiraCPService); + + mStarted = succeed; + return succeed; + } + + @Override + public boolean stop() { + if (mBluetoothGattServer == null) { + Log.w(TAG, "stop failed due to mBluetoothGattServer is null."); + return false; + } + boolean succeed = mBluetoothGattServer.removeService(mFiraCPService); + + // Clear in/out message queue. + mIncompleteInDataPacketQueue.clear(); + mOutMessageQueue.clear(); + + mStarted = !succeed; + return succeed; + } + + @Override + public boolean sendMessage(int secid, FiraConnectorMessage message) { + if (!isProcessing()) { + Log.w(TAG, "Sent request failed due to server not ready for processing."); + return false; + } + byte[] messageBytes = message.toBytes(); + if (messageBytes.length > mRemoteCapabilities.maxMessageBufferSize) { + Log.w( + TAG, + "Sent request failed due to message size exceeded remote device capabilities."); + return false; + } + mOutMessageQueue.add(new MessagePacket(secid, ByteBuffer.wrap(messageBytes))); + + // No existing meesage in progress, send this message immediately. + if (mOutMessageQueue.size() == 1) { + return processOutDataPacket(); + } + return true; + } + + /** + * Process the next out control data packet from the queue. Notify remote device if new data + * packet is set in the {@link mOutControlPointCharacteristic}. + * + * @return indicate if next out data packet was process successfully. + */ + private boolean processOutDataPacket() { + if (!isProcessing()) { + Log.w(TAG, "processOutDataPacket failed due to server not ready for processing."); + return false; + } + if (mOutMessageQueue.isEmpty()) { + Log.d(TAG, "processOutDataPacket skipped due to empty queue."); + return false; + } + MessagePacket messagePacket = mOutMessageQueue.peek(); + ByteBuffer byteBuffer = messagePacket.messageBytes; + byte[] nextPayload = + new byte + [Math.min( + byteBuffer.remaining(), + mRemoteCapabilities.optimizedDataPacketSize + - FiraConnectorDataPacket.HEADER_SIZE)]; + byteBuffer.get(nextPayload); + + FiraConnectorDataPacket dataPacket = + new FiraConnectorDataPacket( + /*lastChainingPacket=*/ !byteBuffer.hasRemaining(), + messagePacket.secid, + nextPayload); + + if (!byteBuffer.hasRemaining()) { + mOutMessageQueue.pop(); + } + if (!mOutControlPointCharacteristic.setValue(dataPacket.toBytes())) { + Log.w( + TAG, + "processOutDataPacket failed due to fail to set" + + " mOutControlPointCharacteristic."); + return false; + } + if (!mBluetoothGattServer.notifyCharacteristicChanged( + mRemoteGattDevice, mOutControlPointCharacteristic, /*confirm=*/ false)) { + Log.w(TAG, "processOutDataPacket failed due to fail to notifyCharacteristicChanged."); + return false; + } + return true; + } + + /** + * Process the next in control data packet. Construct the FiraConnectorMEssage if data is + * complete, and notify callback with the constructed message. + * + * @return indicate if next in data packet was process successfully. + */ + private boolean processInDataPacket(byte[] bytes) { + if (!isProcessing()) { + Log.w(TAG, "processInDataPacket failed due to server not ready for processing."); + return false; + } + FiraConnectorDataPacket latestDataPacket = FiraConnectorDataPacket.fromBytes(bytes); + if (latestDataPacket == null) { + Log.w( + TAG, + "processInDataPacket failed due to latest FiraConnectorDataPacket cannot be" + + " constructed from bytes."); + return false; + } + if (!mIncompleteInDataPacketQueue.isEmpty() + && latestDataPacket.secid != mIncompleteInDataPacketQueue.peek().secid) { + Log.w( + TAG, + "processInDataPacket failed due to latest FiraConnectorDataPacket's SECID" + + " doesn't match previous data packet."); + return false; + } + mIncompleteInDataPacketQueue.add(latestDataPacket); + if (!latestDataPacket.lastChainingPacket) { + return true; + } + // All data packets of the message has been received. Constructing the message. + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + for (FiraConnectorDataPacket dataPacket : mIncompleteInDataPacketQueue) { + byteStream.write(dataPacket.payload, /*off=*/ 0, dataPacket.payload.length); + } + mIncompleteInDataPacketQueue.clear(); + + FiraConnectorMessage message = FiraConnectorMessage.fromBytes(byteStream.toByteArray()); + if (message == null) { + Log.w( + TAG, + "processInDataPacket failed due to FiraConnectorMessage cannot be constructed" + + " from bytes."); + return false; + } + + mTransportServerCallback.onMessage(latestDataPacket.secid, message); + return true; + } + + /** + * Start processing of the FiRa Connector Data Packets and the FiRa Connector Messages through + * the In/Out control point characterstic when all conditions are meet to start the FiRa GATT + * server. + * + * @param device Remote Bluetooth device. + */ + private void startProcessing(BluetoothDevice device) { + if (!mConnected || !mNotificationEnabled || mRemoteCapabilities == null) { + Log.d( + TAG, + "Gatt server not fully ready: connected=" + + mConnected + + ", notification enabled=" + + mNotificationEnabled + + ", valid" + + " capabilities=" + + (mRemoteCapabilities = null)); + boolean stopping = isProcessing(); + mRemoteGattDevice = null; + if (stopping) { + mTransportServerCallback.onProcessingStopped(); + } + return; + } + mRemoteGattDevice = device; + mTransportServerCallback.onProcessingStarted(); + } + + /** + * Start processing of the FiRa Connector Data Packets and the FiRa Connector Messages through + * the In/Out control point characterstic when all conditions are meet to start the FiRa GATT + * server. + * + * @return indicate if server has started processing. + */ + private boolean isProcessing() { + return mRemoteGattDevice != null; + } + + /** + * Initialize all of the GATT characteristics with appropriate default values and the required + * configurations. + */ + private void setupGattCharacteristic() { + mInControlPointCharacteristic = + new BluetoothGattCharacteristic( + UuidConstants.CP_IN_CONTROL_POINT_UUID.getUuid(), + BluetoothGattCharacteristic.PROPERTY_WRITE, + BluetoothGattCharacteristic.PERMISSION_WRITE); + mFiraCPService.addCharacteristic(mInControlPointCharacteristic); + + mOutControlPointCharacteristic = + new BluetoothGattCharacteristic( + UuidConstants.CP_OUT_CONTROL_POINT_UUID.getUuid(), + BluetoothGattCharacteristic.PROPERTY_READ + | BluetoothGattCharacteristic.PROPERTY_NOTIFY, + BluetoothGattCharacteristic.PERMISSION_READ); + mOutControlPointCccdDescriptor = + new BluetoothGattDescriptor( + UuidConstants.CCCD_UUID.getUuid(), BluetoothGattDescriptor.PERMISSION_READ); + mOutControlPointCharacteristic.addDescriptor(mOutControlPointCccdDescriptor); + mFiraCPService.addCharacteristic(mOutControlPointCharacteristic); + + mCapabilitiesCharacteristic = + new BluetoothGattCharacteristic( + UuidConstants.CP_FIRA_CONNECTOR_CAPABILITIES_UUID.getUuid(), + BluetoothGattCharacteristic.PROPERTY_WRITE, + BluetoothGattCharacteristic.PERMISSION_WRITE); + mFiraCPService.addCharacteristic(mCapabilitiesCharacteristic); + } +} diff --git a/service/java/com/android/server/uwb/discovery/ble/UuidConstants.java b/service/java/com/android/server/uwb/discovery/ble/UuidConstants.java new file mode 100644 index 00000000..0bbac7a9 --- /dev/null +++ b/service/java/com/android/server/uwb/discovery/ble/UuidConstants.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.uwb.discovery.ble; + +import android.bluetooth.BluetoothUuid; +import android.os.ParcelUuid; + +/** UUIDs of the UWB BLE OOB. */ +public class UuidConstants { + + /* The FiRa service UUID for connector primary and connector secondary as defined in Bluetooth + * Specification Supplement v10. Little endian encoding. + */ + public static final byte[] FIRA_CP_UUID = new byte[] {(byte) 0xF3, (byte) 0xFF}; + public static final byte[] FIRA_CS_UUID = new byte[] {(byte) 0xF4, (byte) 0xFF}; + + public static final ParcelUuid FIRA_CP_PARCEL_UUID = BluetoothUuid.parseUuidFrom(FIRA_CP_UUID); + public static final ParcelUuid FIRA_CS_PARCEL_UUID = BluetoothUuid.parseUuidFrom(FIRA_CS_UUID); + + /* FiRa Connector Primary GATT Characteristic UUIDs according to FiRa BLE OOB v1.0 + * specification. + */ + public static final ParcelUuid CP_IN_CONTROL_POINT_UUID = + ParcelUuid.fromString("00002A00-0000-1000-8000-D09200000001"); + public static final ParcelUuid CP_OUT_CONTROL_POINT_UUID = + ParcelUuid.fromString("00002A01-0000-1000-8000-D09200000001"); + public static final ParcelUuid CP_FIRA_CONNECTOR_CAPABILITIES_UUID = + ParcelUuid.fromString("00002A02-0000-1000-8000-D09200000001"); + + /* Client Characteristic Configuration Descriptor UUID defined by Bluetooth specification. + */ + public static final ParcelUuid CCCD_UUID = BluetoothUuid.parseUuidFrom(new byte[] {0x29, 0x02}); +} diff --git a/service/java/com/android/server/uwb/discovery/info/FiraConnectorDataPacket.java b/service/java/com/android/server/uwb/discovery/info/FiraConnectorDataPacket.java index 8a3b09d6..0c652ac0 100644 --- a/service/java/com/android/server/uwb/discovery/info/FiraConnectorDataPacket.java +++ b/service/java/com/android/server/uwb/discovery/info/FiraConnectorDataPacket.java @@ -41,6 +41,8 @@ public class FiraConnectorDataPacket { private static final int SECID_BITMASK = 0x7F; + public static final int HEADER_SIZE = 1; + /** True if this the last packet in a fragmented session, otherwise it is false. */ public final boolean lastChainingPacket; diff --git a/service/java/com/android/server/uwb/params/FiraDecoder.java b/service/java/com/android/server/uwb/params/FiraDecoder.java index 276361c2..b821cffc 100644 --- a/service/java/com/android/server/uwb/params/FiraDecoder.java +++ b/service/java/com/android/server/uwb/params/FiraDecoder.java @@ -16,6 +16,8 @@ package com.android.server.uwb.params; +import android.util.Log; + import static com.android.server.uwb.config.CapabilityParam.AOA_AZIMUTH_180; import static com.android.server.uwb.config.CapabilityParam.AOA_AZIMUTH_90; import static com.android.server.uwb.config.CapabilityParam.AOA_ELEVATION; @@ -59,6 +61,7 @@ import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_EXTENDED_M import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_FIRA_MAC_VERSION_RANGE; import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_FIRA_PHY_VERSION_RANGE; import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_HPRF_PARAMETER_SETS; +import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_MIN_RANGING_INTERVAL_MS; import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_MULTI_NODE_MODES; import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_RANGING_METHOD; import static com.android.server.uwb.config.CapabilityParam.SUPPORTED_RFRAME_CONFIG; @@ -88,6 +91,8 @@ import java.util.List; import java.util.stream.IntStream; public class FiraDecoder extends TlvDecoder { + private static final String TAG = "FiraDecoder"; + @Override public <T extends Params> T getParams(TlvDecoderBuffer tlvs, Class<T> paramType) { if (FiraSpecificationParams.class.equals(paramType)) { @@ -108,6 +113,12 @@ public class FiraDecoder extends TlvDecoder { byte[] macVersions = tlvs.getByteArray(SUPPORTED_FIRA_MAC_VERSION_RANGE); builder.setMinMacVersionSupported(FiraProtocolVersion.fromBytes(macVersions, 0)); builder.setMaxMacVersionSupported(FiraProtocolVersion.fromBytes(macVersions, 2)); + try { + int minRangingInterval = tlvs.getInt(SUPPORTED_MIN_RANGING_INTERVAL_MS); + builder.setMinRangingIntervalSupported(minRangingInterval); + } catch (IllegalArgumentException e) { + Log.w(TAG, "SUPPORTED_MIN_RANGING_INTERVAL_MS not found."); + } byte deviceRolesUci = tlvs.getByte(SUPPORTED_DEVICE_ROLES); EnumSet<DeviceRoleCapabilityFlag> deviceRoles = 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 9ddf0748..bd6fbcd7 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 @@ -238,6 +238,21 @@ public class CccOpenRangingParams extends CccParams { mHoppingSequence.set(builder.mHoppingSequence.get()); } + public Builder(@NonNull CccOpenRangingParams params) { + mProtocolVersion.set(params.mProtocolVersion); + mUwbConfig.set(params.mUwbConfig); + mPulseShapeCombo.set(params.mPulseShapeCombo); + mSessionId.set(params.mSessionId); + mRanMultiplier.set(params.mRanMultiplier); + mChannel.set(params.mChannel); + mNumChapsPerSlot.set(params.mNumChapsPerSlot); + mNumResponderNodes.set(params.mNumResponderNodes); + mNumSlotsPerRound.set(params.mNumSlotsPerRound); + mSyncCodeIndex.set(params.mSyncCodeIndex); + mHoppingConfigMode.set(params.mHoppingConfigMode); + mHoppingSequence.set(params.mHoppingSequence); + } + public Builder setProtocolVersion(CccProtocolVersion version) { mProtocolVersion.set(version); return this; 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 d462e312..b6a830b3 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 @@ -926,6 +926,58 @@ public class FiraOpenSessionParams extends FiraParams { mAoaType = builder.mAoaType; } + public Builder(@NonNull FiraOpenSessionParams params) { + mProtocolVersion.set(params.mProtocolVersion); + mSessionId.set(params.mSessionId); + mDeviceType.set(params.mDeviceType); + mDeviceRole.set(params.mDeviceRole); + mRangingRoundUsage = params.mRangingRoundUsage; + mMultiNodeMode.set(params.mMultiNodeMode); + mDeviceAddress = params.mDeviceAddress; + mDestAddressList = params.mDestAddressList; + mInitiationTimeMs = params.mInitiationTimeMs; + mSlotDurationRstu = params.mSlotDurationRstu; + mSlotsPerRangingRound = params.mSlotsPerRangingRound; + mRangingIntervalMs = params.mRangingIntervalMs; + mBlockStrideLength = params.mBlockStrideLength; + mHoppingMode = params.mHoppingMode; + mMaxRangingRoundRetries = params.mMaxRangingRoundRetries; + mSessionPriority = params.mSessionPriority; + mMacAddressMode = params.mMacAddressMode; + mHasResultReportPhase = params.mHasResultReportPhase; + mMeasurementReportType = params.mMeasurementReportType; + mInBandTerminationAttemptCount = params.mInBandTerminationAttemptCount; + mChannelNumber = params.mChannelNumber; + mPreambleCodeIndex = params.mPreambleCodeIndex; + mRframeConfig = params.mRframeConfig; + mPrfMode = params.mPrfMode; + mPreambleDuration = params.mPreambleDuration; + mSfdId = params.mSfdId; + mStsSegmentCount = params.mStsSegmentCount; + mStsLength = params.mStsLength; + mSessionKey = params.mSessionKey; + mSubsessionKey = params.mSubSessionKey; + mPsduDataRate = params.mPsduDataRate; + mBprfPhrDataRate = params.mBprfPhrDataRate; + mFcsType = params.mFcsType; + mIsTxAdaptivePayloadPowerEnabled = params.mIsTxAdaptivePayloadPowerEnabled; + mStsConfig = params.mStsConfig; + mSubSessionId.set(params.mSubSessionId); + mVendorId = params.mVendorId; + mStaticStsIV = params.mStaticStsIV; + mIsKeyRotationEnabled = params.mIsKeyRotationEnabled; + mKeyRotationRate = params.mKeyRotationRate; + mAoaResultRequest = params.mAoaResultRequest; + mRangeDataNtfConfig = params.mRangeDataNtfConfig; + mRangeDataNtfProximityNear = params.mRangeDataNtfProximityNear; + mRangeDataNtfProximityFar = params.mRangeDataNtfProximityFar; + mHasTimeOfFlightReport = params.mHasTimeOfFlightReport; + mHasAngleOfArrivalAzimuthReport = params.mHasAngleOfArrivalAzimuthReport; + mHasAngleOfArrivalElevationReport = params.mHasAngleOfArrivalElevationReport; + mHasAngleOfArrivalFigureOfMeritReport = params.mHasAngleOfArrivalFigureOfMeritReport; + mAoaType = params.mAoaType; + } + public FiraOpenSessionParams.Builder setProtocolVersion(FiraProtocolVersion version) { mProtocolVersion.set(version); return this; diff --git a/service/support_lib/src/com/google/uwb/support/fira/FiraSpecificationParams.java b/service/support_lib/src/com/google/uwb/support/fira/FiraSpecificationParams.java index acff26c2..7cd1ae49 100644 --- a/service/support_lib/src/com/google/uwb/support/fira/FiraSpecificationParams.java +++ b/service/support_lib/src/com/google/uwb/support/fira/FiraSpecificationParams.java @@ -54,6 +54,8 @@ public class FiraSpecificationParams extends FiraParams { private final boolean mHasInitiationTimeSupport; + private final int mMinRangingInterval; + private final EnumSet<MultiNodeCapabilityFlag> mMultiNodeCapabilities; private final EnumSet<PrfCapabilityFlag> mPrfCapabilities; @@ -81,6 +83,7 @@ public class FiraSpecificationParams extends FiraParams { private static final String KEY_BLOCK_STRIDING_SUPPORT = "block_striding"; private static final String KEY_NON_DEFERRED_MODE_SUPPORT = "non_deferred_mode"; private static final String KEY_INITIATION_TIME_SUPPORT = "initiation_time"; + private static final String KEY_MIN_RANGING_INTERVAL = "min_ranging_interval"; private static final String KEY_MULTI_NODE_CAPABILITIES = "multi_node_capabilities"; private static final String KEY_PRF_CAPABILITIES = "prf_capabilities"; private static final String KEY_RANGING_ROUND_CAPABILITIES = "ranging_round_capabilities"; @@ -103,6 +106,7 @@ public class FiraSpecificationParams extends FiraParams { boolean hasBlockStridingSupport, boolean hasNonDeferredModeSupport, boolean hasInitiationTimeSupport, + int minRangingInterval, EnumSet<MultiNodeCapabilityFlag> multiNodeCapabilities, EnumSet<PrfCapabilityFlag> prfCapabilities, EnumSet<RangingRoundCapabilityFlag> rangingRoundCapabilities, @@ -121,6 +125,7 @@ public class FiraSpecificationParams extends FiraParams { mHasBlockStridingSupport = hasBlockStridingSupport; mHasNonDeferredModeSupport = hasNonDeferredModeSupport; mHasInitiationTimeSupport = hasInitiationTimeSupport; + mMinRangingInterval = minRangingInterval; mMultiNodeCapabilities = multiNodeCapabilities; mPrfCapabilities = prfCapabilities; mRangingRoundCapabilities = rangingRoundCapabilities; @@ -176,6 +181,10 @@ public class FiraSpecificationParams extends FiraParams { return mHasInitiationTimeSupport; } + public int getMinRangingInterval() { + return mMinRangingInterval; + } + public EnumSet<MultiNodeCapabilityFlag> getMultiNodeCapabilities() { return mMultiNodeCapabilities; } @@ -229,6 +238,7 @@ public class FiraSpecificationParams extends FiraParams { bundle.putBoolean(KEY_BLOCK_STRIDING_SUPPORT, mHasBlockStridingSupport); bundle.putBoolean(KEY_NON_DEFERRED_MODE_SUPPORT, mHasNonDeferredModeSupport); bundle.putBoolean(KEY_INITIATION_TIME_SUPPORT, mHasInitiationTimeSupport); + bundle.putInt(KEY_MIN_RANGING_INTERVAL, mMinRangingInterval); bundle.putInt(KEY_MULTI_NODE_CAPABILITIES, FlagEnum.toInt(mMultiNodeCapabilities)); bundle.putInt(KEY_PRF_CAPABILITIES, FlagEnum.toInt(mPrfCapabilities)); bundle.putInt(KEY_RANGING_ROUND_CAPABILITIES, FlagEnum.toInt(mRangingRoundCapabilities)); @@ -287,6 +297,7 @@ public class FiraSpecificationParams extends FiraParams { .hasBlockStridingSupport(bundle.getBoolean(KEY_BLOCK_STRIDING_SUPPORT)) .hasNonDeferredModeSupport(bundle.getBoolean(KEY_NON_DEFERRED_MODE_SUPPORT)) .hasInitiationTimeSupport(bundle.getBoolean(KEY_INITIATION_TIME_SUPPORT)) + .setMinRangingIntervalSupported(bundle.getInt(KEY_MIN_RANGING_INTERVAL, -1)) .setMultiNodeCapabilities( FlagEnum.toEnumSet( bundle.getInt(KEY_MULTI_NODE_CAPABILITIES), @@ -345,6 +356,8 @@ public class FiraSpecificationParams extends FiraParams { private boolean mHasInitiationTimeSupport = false; + private int mMinRangingInterval = -1; + // Unicast support is mandatory private final EnumSet<MultiNodeCapabilityFlag> mMultiNodeCapabilities = EnumSet.of(MultiNodeCapabilityFlag.HAS_UNICAST_SUPPORT); @@ -432,6 +445,16 @@ public class FiraSpecificationParams extends FiraParams { return this; } + /** + * Set minimum supported ranging interval + * @param value : minimum ranging interval supported + * @return FiraSpecificationParams builder + */ + public FiraSpecificationParams.Builder setMinRangingIntervalSupported(int value) { + mMinRangingInterval = value; + return this; + } + public FiraSpecificationParams.Builder setMultiNodeCapabilities( Collection<MultiNodeCapabilityFlag> multiNodeCapabilities) { mMultiNodeCapabilities.addAll(multiNodeCapabilities); @@ -496,6 +519,7 @@ public class FiraSpecificationParams extends FiraParams { mHasBlockStridingSupport, mHasNonDeferredModeSupport, mHasInitiationTimeSupport, + mMinRangingInterval, mMultiNodeCapabilities, mPrfCapabilities, mRangingRoundCapabilities, diff --git a/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java b/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java index e25f7abc..29e49e2b 100644 --- a/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java +++ b/service/tests/src/com/android/server/uwb/UwbServiceCoreTest.java @@ -42,7 +42,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.validateMockitoUsage; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -166,8 +166,6 @@ public class UwbServiceCoreTest { when(powerManager.newWakeLock(anyInt(), anyString())) .thenReturn(mock(PowerManager.WakeLock.class)); when(mContext.getSystemService(PowerManager.class)).thenReturn(powerManager); - when(mUwbInjector.isSystemApp(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(true); - when(mUwbInjector.isForegroundAppOrService(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(true); when(mUwbInjector.getDeviceConfigFacade()).thenReturn(mDeviceConfigFacade); when(mDeviceConfigFacade.getBugReportMinIntervalMs()) .thenReturn(DeviceConfigFacade.DEFAULT_BUG_REPORT_MIN_INTERVAL_MS); @@ -372,106 +370,6 @@ public class UwbServiceCoreTest { } @Test - public void testOpenRangingWithNonSystemAppInFg() throws Exception { - enableUwb(); - - when(mUwbInjector.isSystemApp(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(false); - when(mUwbInjector.isForegroundAppOrService(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(true); - - SessionHandle sessionHandle = mock(SessionHandle.class); - IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class); - AttributionSource attributionSource = TEST_ATTRIBUTION_SOURCE; - FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build(); - mUwbServiceCore.openRanging( - attributionSource, sessionHandle, cb, params.toBundle()); - - verify(mUwbSessionManager).initSession( - eq(attributionSource), - eq(sessionHandle), eq(params.getSessionId()), eq(FiraParams.PROTOCOL_NAME), - argThat(p -> ((FiraOpenSessionParams) p).getSessionId() == params.getSessionId()), - eq(cb)); - } - - @Test - public void testOpenRangingWithNonSystemAppNotInFg() throws Exception { - enableUwb(); - - when(mUwbInjector.isSystemApp(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(false); - when(mUwbInjector.isForegroundAppOrService(TEST_UID, TEST_PACKAGE_NAME)).thenReturn(false); - - SessionHandle sessionHandle = mock(SessionHandle.class); - IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class); - AttributionSource attributionSource = TEST_ATTRIBUTION_SOURCE; - FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build(); - mUwbServiceCore.openRanging( - attributionSource, sessionHandle, cb, params.toBundle()); - - verify(mUwbSessionManager, never()).initSession( - any(), any(), anyInt(), any(), any(), any()); - verify(cb).onRangingOpenFailed( - eq(sessionHandle), eq(StateChangeReason.SYSTEM_POLICY), any()); - } - - @Test - public void testOpenRangingWithNonSystemAppInFgInChain() throws Exception { - enableUwb(); - - int test_uid_2 = 67; - String test_package_name_2 = "com.android.uwb.2"; - when(mUwbInjector.isSystemApp(test_uid_2, test_package_name_2)).thenReturn(false); - when(mUwbInjector.isForegroundAppOrService(test_uid_2, test_package_name_2)) - .thenReturn(true); - - SessionHandle sessionHandle = mock(SessionHandle.class); - IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class); - // simulate system app triggered the request on behalf of a fg app in fg. - AttributionSource attributionSource = new AttributionSource.Builder(TEST_UID) - .setPackageName(TEST_PACKAGE_NAME) - .setNext(new AttributionSource.Builder(test_uid_2) - .setPackageName(test_package_name_2) - .build()) - .build(); - FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build(); - mUwbServiceCore.openRanging( - attributionSource, sessionHandle, cb, params.toBundle()); - - verify(mUwbSessionManager).initSession( - eq(attributionSource), - eq(sessionHandle), eq(params.getSessionId()), eq(FiraParams.PROTOCOL_NAME), - argThat(p -> ((FiraOpenSessionParams) p).getSessionId() == params.getSessionId()), - eq(cb)); - } - - @Test - public void testOpenRangingWithNonSystemAppNotInFgInChain() throws Exception { - enableUwb(); - - int test_uid_2 = 67; - String test_package_name_2 = "com.android.uwb.2"; - when(mUwbInjector.isSystemApp(test_uid_2, test_package_name_2)).thenReturn(false); - when(mUwbInjector.isForegroundAppOrService(test_uid_2, test_package_name_2)) - .thenReturn(false); - - SessionHandle sessionHandle = mock(SessionHandle.class); - IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class); - // simulate system app triggered the request on behalf of a fg app not in fg. - AttributionSource attributionSource = new AttributionSource.Builder(TEST_UID) - .setPackageName(TEST_PACKAGE_NAME) - .setNext(new AttributionSource.Builder(test_uid_2) - .setPackageName(test_package_name_2) - .build()) - .build(); - FiraOpenSessionParams params = TEST_FIRA_OPEN_SESSION_PARAMS.build(); - mUwbServiceCore.openRanging( - attributionSource, sessionHandle, cb, params.toBundle()); - - verify(mUwbSessionManager, never()).initSession( - any(), any(), anyInt(), any(), any(), any()); - verify(cb).onRangingOpenFailed( - eq(sessionHandle), eq(StateChangeReason.SYSTEM_POLICY), any()); - } - - @Test public void testStartCccRanging() throws Exception { enableUwb(); @@ -652,6 +550,7 @@ public class UwbServiceCoreTest { StateChangeReason.SYSTEM_POLICY); when(mNativeUwbManager.doDeinitialize()).thenReturn(true); + when(mNativeUwbManager.doInitialize()).thenReturn(true); mUwbServiceCore.onDeviceStatusNotificationReceived(UwbUciConstants.DEVICE_STATE_ERROR); mTestLooper.dispatchAll(); @@ -659,6 +558,12 @@ public class UwbServiceCoreTest { verify(mNativeUwbManager).doDeinitialize(); verify(cb).onAdapterStateChanged(UwbManager.AdapterStateCallback.STATE_DISABLED, StateChangeReason.SYSTEM_POLICY); + + // Verify UWB toggle on. + verify(mNativeUwbManager, times(2)).doInitialize(); + verify(cb, times(2)).onAdapterStateChanged( + UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE, + StateChangeReason.SYSTEM_POLICY); } @Test diff --git a/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java b/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java index cb0ca82e..bbe028c9 100644 --- a/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java +++ b/service/tests/src/com/android/server/uwb/UwbSessionManagerTest.java @@ -16,6 +16,8 @@ package com.android.server.uwb; +import static com.android.server.uwb.UwbSessionManager.SESSION_OPEN_RANGING; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -43,6 +45,7 @@ import android.os.test.TestLooper; import android.uwb.IUwbRangingCallbacks; import android.uwb.RangingChangeReason; import android.uwb.SessionHandle; +import android.uwb.StateChangeReason; import android.uwb.UwbAddress; import com.android.dx.mockito.inline.extended.ExtendedMockito; @@ -110,6 +113,8 @@ public class UwbSessionManagerTest { public void setup() { MockitoAnnotations.initMocks(this); when(mNativeUwbManager.getMaxSessionNumber()).thenReturn(MAX_SESSION_NUM); + when(mUwbInjector.isSystemApp(UID, PACKAGE_NAME)).thenReturn(true); + when(mUwbInjector.isForegroundAppOrService(UID, PACKAGE_NAME)).thenReturn(true); // TODO: Don't use spy. mUwbSessionManager = spy(new UwbSessionManager( @@ -608,25 +613,7 @@ public class UwbSessionManagerTest { assertThat(actualStatus).isEqualTo(UwbUciConstants.STATUS_CODE_ERROR_SESSION_NOT_EXIST); } - @Test - public void reconfigure_calledSuccess() { - doReturn(true).when(mUwbSessionManager).isExistedSession(any()); - FiraRangingReconfigureParams params = - new FiraRangingReconfigureParams.Builder() - .setBlockStrideLength(10) - .setRangeDataNtfConfig(1) - .setRangeDataProximityFar(10) - .setRangeDataProximityNear(2) - .build(); - - int actualStatus = mUwbSessionManager.reconfigure(mock(SessionHandle.class), params); - - assertThat(actualStatus).isEqualTo(0); - assertThat(mTestLooper.nextMessage().what) - .isEqualTo(4); // SESSION_RECONFIG_RANGING - } - - private UwbSession setUpUwbSessionForExecution() throws RemoteException { + private UwbSession setUpUwbSessionForExecution(AttributionSource attributionSource) { // setup message doReturn(0).when(mUwbSessionManager).getSessionCount(); doReturn(false).when(mUwbSessionManager).isExistedSession(anyInt()); @@ -647,7 +634,7 @@ public class UwbSessionManagerTest { .build(); IBinder mockBinder = mock(IBinder.class); UwbSession uwbSession = spy( - mUwbSessionManager.new UwbSession(ATTRIBUTION_SOURCE, mockSessionHandle, + mUwbSessionManager.new UwbSession(attributionSource, mockSessionHandle, TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, params, mockRangingCallbacks)); doReturn(mockBinder).when(uwbSession).getBinder(); doReturn(uwbSession).when(mUwbSessionManager).createUwbSession(any(), any(), anyInt(), @@ -694,7 +681,7 @@ public class UwbSessionManagerTest { @Test public void openRanging_success() throws Exception { - UwbSession uwbSession = setUpUwbSessionForExecution(); + UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE); // stub for openRanging conditions when(mNativeUwbManager.initSession(anyInt(), anyByte())) .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK); @@ -718,7 +705,7 @@ public class UwbSessionManagerTest { @Test public void openRanging_timeout() throws Exception { - UwbSession uwbSession = setUpUwbSessionForExecution(); + UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE); // stub for openRanging conditions when(mNativeUwbManager.initSession(anyInt(), anyByte())) .thenThrow(new IllegalStateException()); @@ -742,7 +729,7 @@ public class UwbSessionManagerTest { @Test public void openRanging_nativeInitSessionFailed() throws Exception { - UwbSession uwbSession = setUpUwbSessionForExecution(); + UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE); // stub for openRanging conditions when(mNativeUwbManager.initSession(anyInt(), anyByte())) .thenReturn((byte) UwbUciConstants.STATUS_CODE_FAILED); @@ -766,7 +753,7 @@ public class UwbSessionManagerTest { @Test public void openRanging_setAppConfigurationFailed() throws Exception { - UwbSession uwbSession = setUpUwbSessionForExecution(); + UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE); // stub for openRanging conditions when(mNativeUwbManager.initSession(anyInt(), anyByte())) .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK); @@ -790,7 +777,7 @@ public class UwbSessionManagerTest { @Test public void openRanging_wrongInitState() throws Exception { - UwbSession uwbSession = setUpUwbSessionForExecution(); + UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE); // stub for openRanging conditions when(mNativeUwbManager.initSession(anyInt(), anyByte())) .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK); @@ -814,7 +801,7 @@ public class UwbSessionManagerTest { @Test public void openRanging_wrongIdleState() throws Exception { - UwbSession uwbSession = setUpUwbSessionForExecution(); + UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE); // stub for openRanging conditions when(mNativeUwbManager.initSession(anyInt(), anyByte())) .thenReturn((byte) UwbUciConstants.STATUS_CODE_OK); @@ -837,8 +824,92 @@ public class UwbSessionManagerTest { verify(mNativeUwbManager).deInitSession(eq(TEST_SESSION_ID)); } + @Test + public void testInitSessionWithNonSystemAppInFg() throws Exception { + when(mUwbInjector.isSystemApp(UID, PACKAGE_NAME)).thenReturn(false); + when(mUwbInjector.isForegroundAppOrService(UID, PACKAGE_NAME)).thenReturn(true); + + UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE); + mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(), + TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks()); + + // OPEN_RANGING message scheduled. + assertThat(mTestLooper.nextMessage().what).isEqualTo(SESSION_OPEN_RANGING); + assertThat(mTestLooper.isIdle()).isFalse(); + } + + @Test + public void testInitSessionWithNonSystemAppNotInFg() throws Exception { + when(mUwbInjector.isSystemApp(UID, PACKAGE_NAME)).thenReturn(false); + when(mUwbInjector.isForegroundAppOrService(UID, PACKAGE_NAME)).thenReturn(false); + + UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE); + mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(), + TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks()); + + verify(uwbSession.getIUwbRangingCallbacks()).onRangingOpenFailed( + eq(uwbSession.getSessionHandle()), eq(StateChangeReason.SYSTEM_POLICY), any()); + // No OPEN_RANGING message scheduled. + assertThat(mTestLooper.isIdle()).isFalse(); + } + + @Test + public void testOpenRangingWithNonSystemAppInFgInChain() throws Exception { + int test_uid_2 = 67; + String test_package_name_2 = "com.android.uwb.2"; + when(mUwbInjector.isSystemApp(test_uid_2, test_package_name_2)).thenReturn(false); + when(mUwbInjector.isForegroundAppOrService(test_uid_2, test_package_name_2)) + .thenReturn(true); + + // simulate system app triggered the request on behalf of a fg app in fg. + AttributionSource attributionSource = new AttributionSource.Builder(UID) + .setPackageName(PACKAGE_NAME) + .setNext(new AttributionSource.Builder(test_uid_2) + .setPackageName(test_package_name_2) + .build()) + .build(); + + UwbSession uwbSession = setUpUwbSessionForExecution(attributionSource); + + mUwbSessionManager.initSession(attributionSource, uwbSession.getSessionHandle(), + TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks()); + + // OPEN_RANGING message scheduled. + assertThat(mTestLooper.nextMessage().what).isEqualTo(SESSION_OPEN_RANGING); + assertThat(mTestLooper.isIdle()).isFalse(); + } + + @Test + public void testOpenRangingWithNonSystemAppNotInFgInChain() throws Exception { + int test_uid_2 = 67; + String test_package_name_2 = "com.android.uwb.2"; + when(mUwbInjector.isSystemApp(test_uid_2, test_package_name_2)).thenReturn(false); + when(mUwbInjector.isForegroundAppOrService(test_uid_2, test_package_name_2)) + .thenReturn(false); + + // simulate system app triggered the request on behalf of a fg app not in fg. + AttributionSource attributionSource = new AttributionSource.Builder(UID) + .setPackageName(PACKAGE_NAME) + .setNext(new AttributionSource.Builder(test_uid_2) + .setPackageName(test_package_name_2) + .build()) + .build(); + UwbSession uwbSession = setUpUwbSessionForExecution(attributionSource); + mUwbSessionManager.initSession(attributionSource, uwbSession.getSessionHandle(), + TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, + uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks()); + + verify(uwbSession.getIUwbRangingCallbacks()).onRangingOpenFailed( + eq(uwbSession.getSessionHandle()), eq(StateChangeReason.SYSTEM_POLICY), any()); + // No OPEN_RANGING message scheduled. + assertThat(mTestLooper.isIdle()).isFalse(); + } + private UwbSession prepareExistingUwbSession() throws Exception { - UwbSession uwbSession = setUpUwbSessionForExecution(); + UwbSession uwbSession = setUpUwbSessionForExecution(ATTRIBUTION_SOURCE); mUwbSessionManager.initSession(ATTRIBUTION_SOURCE, uwbSession.getSessionHandle(), TEST_SESSION_ID, FiraParams.PROTOCOL_NAME, uwbSession.getParams(), uwbSession.getIUwbRangingCallbacks()); @@ -862,6 +933,31 @@ public class UwbSessionManagerTest { } @Test + public void reconfigure_calledSuccess() throws Exception { + UwbSession uwbSession = prepareExistingUwbSession(); + FiraRangingReconfigureParams params = + new FiraRangingReconfigureParams.Builder() + .setBlockStrideLength(10) + .setRangeDataNtfConfig(1) + .setRangeDataProximityFar(10) + .setRangeDataProximityNear(2) + .build(); + + int actualStatus = mUwbSessionManager.reconfigure(uwbSession.getSessionHandle(), params); + + assertThat(actualStatus).isEqualTo(0); + assertThat(mTestLooper.nextMessage().what) + .isEqualTo(UwbSessionManager.SESSION_RECONFIG_RANGING); + + // Verify the cache has been updated. + FiraOpenSessionParams firaParams = (FiraOpenSessionParams) uwbSession.getParams(); + assertThat(firaParams.getBlockStrideLength()).isEqualTo(10); + assertThat(firaParams.getRangeDataNtfConfig()).isEqualTo(1); + assertThat(firaParams.getRangeDataNtfProximityFar()).isEqualTo(10); + assertThat(firaParams.getRangeDataNtfProximityNear()).isEqualTo(2); + } + + @Test public void startRanging_sessionStateIdle() throws Exception { UwbSession uwbSession = prepareExistingUwbSession(); // set up for start ranging diff --git a/service/tests/src/com/android/server/uwb/discovery/TransportServerServiceTest.java b/service/tests/src/com/android/server/uwb/discovery/TransportServerServiceTest.java new file mode 100644 index 00000000..40603ef4 --- /dev/null +++ b/service/tests/src/com/android/server/uwb/discovery/TransportServerServiceTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.uwb.discovery; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothGattServer; +import android.bluetooth.BluetoothGattServerCallback; +import android.bluetooth.BluetoothManager; +import android.content.AttributionSource; +import android.content.Context; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.uwb.discovery.TransportServerProvider.TransportServerCallback; +import com.android.server.uwb.discovery.info.DiscoveryInfo; +import com.android.server.uwb.discovery.info.DiscoveryInfo.TransportType; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +/** Unit test for {@link TransportServerService} */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class TransportServerServiceTest { + + private static final DiscoveryInfo DISCOVERY_INFO = + new DiscoveryInfo(TransportType.BLE, Optional.empty(), Optional.empty()); + + @Mock AttributionSource mMockAttributionSource; + @Mock Context mMockContext; + @Mock BluetoothManager mMockBluetoothManager; + @Mock BluetoothAdapter mMockBluetoothAdapter; + @Mock BluetoothGattServer mMockBluetoothGattServer; + @Mock TransportServerCallback mMockTransportServerCallback; + + private TransportServerService mTransportServerService; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + when(mMockContext.createContext(any())).thenReturn(mMockContext); + when(mMockContext.getSystemService(BluetoothManager.class)) + .thenReturn(mMockBluetoothManager); + when(mMockBluetoothManager.getAdapter()).thenReturn(mMockBluetoothAdapter); + when(mMockBluetoothManager.openGattServer( + eq(mMockContext), any(BluetoothGattServerCallback.class))) + .thenReturn(mMockBluetoothGattServer); + + mTransportServerService = + new TransportServerService( + mMockAttributionSource, + mMockContext, + DISCOVERY_INFO, + mMockTransportServerCallback); + } + + @Test + public void testStart_failed() { + when(mMockBluetoothGattServer.addService(any())).thenReturn(false); + assertThat(mTransportServerService.start()).isFalse(); + verify(mMockBluetoothGattServer, times(1)).addService(any()); + } + + @Test + public void testStart_successAndRejectRestart() { + when(mMockBluetoothGattServer.addService(any())).thenReturn(true); + assertThat(mTransportServerService.start()).isTrue(); + verify(mMockBluetoothGattServer, times(1)).addService(any()); + assertThat(mTransportServerService.start()).isFalse(); + verify(mMockBluetoothGattServer, times(1)).addService(any()); + } + + @Test + public void testStop_failed() { + when(mMockBluetoothGattServer.addService(any())).thenReturn(true); + when(mMockBluetoothGattServer.removeService(any())).thenReturn(false); + assertThat(mTransportServerService.start()).isTrue(); + verify(mMockBluetoothGattServer, times(1)).addService(any()); + assertThat(mTransportServerService.stop()).isFalse(); + verify(mMockBluetoothGattServer, times(1)).removeService(any()); + } + + @Test + public void testStop_successAndRejectRestop() { + when(mMockBluetoothGattServer.addService(any())).thenReturn(true); + when(mMockBluetoothGattServer.removeService(any())).thenReturn(true); + assertThat(mTransportServerService.start()).isTrue(); + verify(mMockBluetoothGattServer, times(1)).addService(any()); + assertThat(mTransportServerService.stop()).isTrue(); + verify(mMockBluetoothGattServer, times(1)).removeService(any()); + assertThat(mTransportServerService.stop()).isFalse(); + verify(mMockBluetoothGattServer, times(1)).removeService(any()); + } +} diff --git a/service/tests/src/com/android/server/uwb/discovery/ble/GattTransportServerProviderTest.java b/service/tests/src/com/android/server/uwb/discovery/ble/GattTransportServerProviderTest.java new file mode 100644 index 00000000..cb328dd5 --- /dev/null +++ b/service/tests/src/com/android/server/uwb/discovery/ble/GattTransportServerProviderTest.java @@ -0,0 +1,522 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.uwb.discovery.ble; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattServer; +import android.bluetooth.BluetoothGattServerCallback; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.content.AttributionSource; +import android.content.Context; +import android.uwb.UwbTestUtils; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.uwb.discovery.TransportServerProvider.TransportServerCallback; +import com.android.server.uwb.discovery.info.FiraConnectorCapabilities; +import com.android.server.uwb.discovery.info.FiraConnectorDataPacket; +import com.android.server.uwb.discovery.info.FiraConnectorMessage; +import com.android.server.uwb.discovery.info.FiraConnectorMessage.InstructionCode; +import com.android.server.uwb.discovery.info.FiraConnectorMessage.MessageType; +import com.android.server.uwb.discovery.info.SecureComponentInfo; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.Arrays; +import java.util.concurrent.Executor; + +/** Unit test for {@link GattTransportServerProvider} */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class GattTransportServerProviderTest { + + private static final Executor EXECUTOR = UwbTestUtils.getExecutor(); + private static final int SECID = 10; + private static final byte[] MESSAGE_PAYLOAD1 = new byte[] {(byte) 0xF4, 0x00, 0x40}; + private static final FiraConnectorMessage MESSAGE = + new FiraConnectorMessage( + MessageType.EVENT, InstructionCode.DATA_EXCHANGE, MESSAGE_PAYLOAD1); + private static final FiraConnectorDataPacket DATA_PACKET = + new FiraConnectorDataPacket(/*lastChainingPacket=*/ true, SECID, MESSAGE.toBytes()); + private static final int OPTIMIZED_DATA_PACKET_SIZE = 21; + private static final FiraConnectorCapabilities CAPABILITIES = + new FiraConnectorCapabilities.Builder() + .setOptimizedDataPacketSize(OPTIMIZED_DATA_PACKET_SIZE) + .setMaxMessageBufferSize(265) + .addSecureComponentInfo( + new SecureComponentInfo( + /*static_indication=*/ true, + SECID, + SecureComponentInfo.SecureComponentType.ESE_NONREMOVABLE, + SecureComponentInfo.SecureComponentProtocolType + .FIRA_OOB_ADMINISTRATIVE_PROTOCOL)) + .build(); + private static final BluetoothGattCharacteristic IN_CHARACTERSTIC = + new BluetoothGattCharacteristic( + UuidConstants.CP_IN_CONTROL_POINT_UUID.getUuid(), + BluetoothGattCharacteristic.PROPERTY_WRITE, + BluetoothGattCharacteristic.PERMISSION_WRITE); + private static final BluetoothGattCharacteristic OUT_CHARACTERSTIC = + new BluetoothGattCharacteristic( + UuidConstants.CP_OUT_CONTROL_POINT_UUID.getUuid(), + BluetoothGattCharacteristic.PROPERTY_READ + | BluetoothGattCharacteristic.PROPERTY_NOTIFY, + BluetoothGattCharacteristic.PERMISSION_READ); + private static final BluetoothGattCharacteristic CAPABILITIES_CHARACTERSTIC = + new BluetoothGattCharacteristic( + UuidConstants.CP_FIRA_CONNECTOR_CAPABILITIES_UUID.getUuid(), + BluetoothGattCharacteristic.PROPERTY_WRITE, + BluetoothGattCharacteristic.PERMISSION_WRITE); + private static final BluetoothGattDescriptor CCCD_DESCRIPTOR = + new BluetoothGattDescriptor( + UuidConstants.CCCD_UUID.getUuid(), BluetoothGattDescriptor.PERMISSION_READ); + + @Mock AttributionSource mMockAttributionSource; + @Mock Context mMockContext; + @Mock BluetoothManager mMockBluetoothManager; + @Mock BluetoothAdapter mMockBluetoothAdapter; + @Mock BluetoothGattServer mMockBluetoothGattServer; + @Mock TransportServerCallback mMockTransportServerCallback; + @Mock BluetoothDevice mMockBluetoothDevice; + + private GattTransportServerProvider mGattTransportServerProvider; + private BluetoothGattServerCallback mBluetoothGattServerCallback; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + when(mMockContext.createContext(any())).thenReturn(mMockContext); + when(mMockContext.getSystemService(BluetoothManager.class)) + .thenReturn(mMockBluetoothManager); + when(mMockBluetoothManager.getAdapter()).thenReturn(mMockBluetoothAdapter); + when(mMockBluetoothManager.openGattServer(eq(mMockContext), any())) + .thenReturn(mMockBluetoothGattServer); + when(mMockBluetoothGattServer.addService(any())).thenReturn(true); + when(mMockBluetoothGattServer.removeService(any())).thenReturn(true); + + mGattTransportServerProvider = + new GattTransportServerProvider( + mMockAttributionSource, mMockContext, mMockTransportServerCallback); + + ArgumentCaptor<BluetoothGattServerCallback> captor = + ArgumentCaptor.forClass(BluetoothGattServerCallback.class); + verify(mMockBluetoothManager, times(1)).openGattServer(eq(mMockContext), captor.capture()); + mBluetoothGattServerCallback = captor.getValue(); + assertThat(mBluetoothGattServerCallback).isNotNull(); + } + + @Test + public void testStartAndStop() { + assertThat(mGattTransportServerProvider.start()).isTrue(); + verify(mMockBluetoothGattServer, times(1)).addService(any()); + assertThat(mGattTransportServerProvider.isStarted()).isTrue(); + assertThat(mGattTransportServerProvider.stop()).isTrue(); + verify(mMockBluetoothGattServer, times(1)).removeService(any()); + assertThat(mGattTransportServerProvider.isStarted()).isFalse(); + } + + @Test + public void testStartProcessing_succeed() { + mBluetoothGattServerCallback.onConnectionStateChange( + mMockBluetoothDevice, /*status=*/ 1, BluetoothProfile.STATE_CONNECTED); + mBluetoothGattServerCallback.onDescriptorWriteRequest( + mMockBluetoothDevice, + /*requestId=*/ 1, + CCCD_DESCRIPTOR, + /*preparedWrite=*/ false, + /*responseNeeded=*/ true, + /*offset=*/ 0, + BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + mBluetoothGattServerCallback.onCharacteristicWriteRequest( + mMockBluetoothDevice, + /*requestId=*/ 2, + CAPABILITIES_CHARACTERSTIC, + /*preparedWrite=*/ false, + /*responseNeeded=*/ true, + /*offset=*/ 0, + CAPABILITIES.toBytes()); + + verify(mMockBluetoothGattServer, times(1)) + .sendResponse( + mMockBluetoothDevice, + /*requestId=*/ 1, + BluetoothGatt.GATT_SUCCESS, + /*offset=*/ 0, + BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + verify(mMockBluetoothGattServer, times(1)) + .sendResponse( + mMockBluetoothDevice, + /*requestId=*/ 2, + BluetoothGatt.GATT_SUCCESS, + /*offset=*/ 0, + CAPABILITIES.toBytes()); + ArgumentCaptor<FiraConnectorCapabilities> captor = + ArgumentCaptor.forClass(FiraConnectorCapabilities.class); + verify(mMockTransportServerCallback, times(1)).onCapabilitesUpdated(captor.capture()); + assertThat(captor.getValue().toString()).isEqualTo(CAPABILITIES.toString()); + verify(mMockTransportServerCallback, times(1)).onProcessingStarted(); + verify(mMockTransportServerCallback, never()).onProcessingStopped(); + } + + private void startProcessing() { + mBluetoothGattServerCallback.onConnectionStateChange( + mMockBluetoothDevice, /*status=*/ 1, BluetoothProfile.STATE_CONNECTED); + mBluetoothGattServerCallback.onDescriptorWriteRequest( + mMockBluetoothDevice, + /*requestId=*/ 1, + CCCD_DESCRIPTOR, + /*preparedWrite=*/ false, + /*responseNeeded=*/ false, + /*offset=*/ 0, + BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + mBluetoothGattServerCallback.onCharacteristicWriteRequest( + mMockBluetoothDevice, + /*requestId=*/ 2, + CAPABILITIES_CHARACTERSTIC, + /*preparedWrite=*/ false, + /*responseNeeded=*/ false, + /*offset=*/ 0, + CAPABILITIES.toBytes()); + } + + @Test + public void testSendMessage_succeed() { + when(mMockBluetoothGattServer.notifyCharacteristicChanged( + eq(mMockBluetoothDevice), any(), eq(false))) + .thenReturn(true); + + startProcessing(); + + assertThat(mGattTransportServerProvider.sendMessage(SECID, MESSAGE)).isTrue(); + } + + @Test + public void testSendMessage_failedProcessingNotStarted() { + when(mMockBluetoothGattServer.notifyCharacteristicChanged( + eq(mMockBluetoothDevice), any(), eq(false))) + .thenReturn(true); + + assertThat(mGattTransportServerProvider.sendMessage(SECID, MESSAGE)).isFalse(); + } + + @Test + public void testSendMessage_failedMessageLengthGreaterThanCapabilitites() { + when(mMockBluetoothGattServer.notifyCharacteristicChanged( + eq(mMockBluetoothDevice), any(), eq(false))) + .thenReturn(true); + byte[] bytes = new byte[270]; + Arrays.fill(bytes, (byte) 1); + FiraConnectorMessage message = + new FiraConnectorMessage(MessageType.EVENT, InstructionCode.DATA_EXCHANGE, bytes); + + startProcessing(); + + // Capabilities set the max message length to 265, so a message of length 270 exceeded the + // limit. + assertThat(mGattTransportServerProvider.sendMessage(SECID, message)).isFalse(); + } + + @Test + public void testSendMessage_failedNotifyCharacteristicChangedFailed() { + when(mMockBluetoothGattServer.notifyCharacteristicChanged( + eq(mMockBluetoothDevice), any(), eq(false))) + .thenReturn(false); + + startProcessing(); + + assertThat(mGattTransportServerProvider.sendMessage(SECID, MESSAGE)).isFalse(); + } + + private void setupOutCharactersticRead() { + Answer notifyOutCharacteristicChangedResponse = + new Answer() { + public Object answer(InvocationOnMock invocation) { + EXECUTOR.execute( + () -> + mBluetoothGattServerCallback.onCharacteristicReadRequest( + mMockBluetoothDevice, + /*requestId=*/ 3, + /*offset=*/ 0, + OUT_CHARACTERSTIC)); + return true; + } + }; + doAnswer(notifyOutCharacteristicChangedResponse) + .when(mMockBluetoothGattServer) + .notifyCharacteristicChanged(eq(mMockBluetoothDevice), any(), eq(false)); + } + + @Test + public void testSendMessageAndOutCharactersticRead_succeed() { + setupOutCharactersticRead(); + startProcessing(); + + assertThat(mGattTransportServerProvider.sendMessage(SECID, MESSAGE)).isTrue(); + verify(mMockBluetoothGattServer, times(1)) + .sendResponse( + mMockBluetoothDevice, + /*requestId=*/ 3, + BluetoothGatt.GATT_SUCCESS, + /*offset=*/ 0, + DATA_PACKET.toBytes()); + } + + @Test + public void testSendMessageAndOutCharactersticRead_threeReadSucceed() { + byte[] messagePayload = new byte[45]; + Arrays.fill(messagePayload, (byte) 2); + FiraConnectorMessage message = + new FiraConnectorMessage( + MessageType.EVENT, InstructionCode.DATA_EXCHANGE, messagePayload); + byte[] messageBytes = message.toBytes(); + int payloadSize = OPTIMIZED_DATA_PACKET_SIZE - 1; + FiraConnectorDataPacket dataPacket1 = + new FiraConnectorDataPacket( + /*lastChainingPacket=*/ false, + SECID, + Arrays.copyOf(messageBytes, payloadSize)); + FiraConnectorDataPacket dataPacket2 = + new FiraConnectorDataPacket( + /*lastChainingPacket=*/ false, + SECID, + Arrays.copyOfRange(messageBytes, payloadSize, 2 * payloadSize)); + FiraConnectorDataPacket dataPacket3 = + new FiraConnectorDataPacket( + /*lastChainingPacket=*/ true, + SECID, + Arrays.copyOfRange(messageBytes, 2 * payloadSize, messageBytes.length)); + + setupOutCharactersticRead(); + startProcessing(); + + assertThat(mGattTransportServerProvider.sendMessage(SECID, message)).isTrue(); + verify(mMockBluetoothGattServer, times(3)) + .sendResponse( + eq(mMockBluetoothDevice), + eq(/*requestId=*/ 3), + eq(BluetoothGatt.GATT_SUCCESS), + eq(/*offset=*/ 0), + any()); + verify(mMockBluetoothGattServer, times(1)) + .sendResponse( + mMockBluetoothDevice, + /*requestId=*/ 3, + BluetoothGatt.GATT_SUCCESS, + /*offset=*/ 0, + dataPacket1.toBytes()); + verify(mMockBluetoothGattServer, times(1)) + .sendResponse( + mMockBluetoothDevice, + /*requestId=*/ 3, + BluetoothGatt.GATT_SUCCESS, + /*offset=*/ 0, + dataPacket2.toBytes()); + verify(mMockBluetoothGattServer, times(1)) + .sendResponse( + mMockBluetoothDevice, + /*requestId=*/ 3, + BluetoothGatt.GATT_SUCCESS, + /*offset=*/ 0, + dataPacket3.toBytes()); + } + + @Test + public void testInCharactersticWrite_failedProcessingNotStarted() { + mBluetoothGattServerCallback.onCharacteristicWriteRequest( + mMockBluetoothDevice, + /*requestId=*/ 4, + IN_CHARACTERSTIC, + /*preparedWrite=*/ false, + /*responseNeeded=*/ true, + /*offset=*/ 0, + DATA_PACKET.toBytes()); + + verify(mMockBluetoothGattServer, times(1)) + .sendResponse( + mMockBluetoothDevice, + /*requestId=*/ 4, + BluetoothGatt.GATT_FAILURE, + /*offset=*/ 0, + /*value=*/ null); + verifyZeroInteractions(mMockTransportServerCallback); + } + + @Test + public void testInCharactersticWrite_failedEmptyDataPacket() { + startProcessing(); + mBluetoothGattServerCallback.onCharacteristicWriteRequest( + mMockBluetoothDevice, + /*requestId=*/ 4, + IN_CHARACTERSTIC, + /*preparedWrite=*/ false, + /*responseNeeded=*/ true, + /*offset=*/ 0, + new byte[] {}); + + verify(mMockBluetoothGattServer, times(1)) + .sendResponse( + mMockBluetoothDevice, + /*requestId=*/ 4, + BluetoothGatt.GATT_FAILURE, + /*offset=*/ 0, + /*value=*/ null); + verify(mMockTransportServerCallback, never()).onMessage(anyInt(), any()); + } + + @Test + public void testInCharactersticWrite_noResponse() { + startProcessing(); + mBluetoothGattServerCallback.onCharacteristicWriteRequest( + mMockBluetoothDevice, + /*requestId=*/ 4, + IN_CHARACTERSTIC, + /*preparedWrite=*/ false, + /*responseNeeded=*/ false, + /*offset=*/ 0, + new byte[] {}); + + verify(mMockBluetoothGattServer, never()) + .sendResponse(any(BluetoothDevice.class), anyInt(), anyInt(), anyInt(), any()); + verify(mMockTransportServerCallback, never()).onMessage(anyInt(), any()); + + mBluetoothGattServerCallback.onCharacteristicWriteRequest( + mMockBluetoothDevice, + /*requestId=*/ 4, + IN_CHARACTERSTIC, + /*preparedWrite=*/ false, + /*responseNeeded=*/ false, + /*offset=*/ 0, + DATA_PACKET.toBytes()); + + verify(mMockBluetoothGattServer, never()) + .sendResponse(any(BluetoothDevice.class), anyInt(), anyInt(), anyInt(), any()); + ArgumentCaptor<FiraConnectorMessage> captor = + ArgumentCaptor.forClass(FiraConnectorMessage.class); + verify(mMockTransportServerCallback, times(1)).onMessage(eq(SECID), captor.capture()); + assertThat(captor.getValue().toString()).isEqualTo(MESSAGE.toString()); + } + + @Test + public void testInCharactersticWrite_succeed() { + byte[] messagePayload = new byte[51]; + Arrays.fill(messagePayload, (byte) 3); + FiraConnectorMessage message = + new FiraConnectorMessage( + MessageType.EVENT, InstructionCode.DATA_EXCHANGE, messagePayload); + byte[] messageBytes = message.toBytes(); + int payloadSize = OPTIMIZED_DATA_PACKET_SIZE - 1; + FiraConnectorDataPacket dataPacket1 = + new FiraConnectorDataPacket( + /*lastChainingPacket=*/ false, + SECID, + Arrays.copyOf(messageBytes, payloadSize)); + FiraConnectorDataPacket dataPacket2 = + new FiraConnectorDataPacket( + /*lastChainingPacket=*/ false, + SECID, + Arrays.copyOfRange(messageBytes, payloadSize, 2 * payloadSize)); + FiraConnectorDataPacket dataPacket3 = + new FiraConnectorDataPacket( + /*lastChainingPacket=*/ true, + SECID, + Arrays.copyOfRange(messageBytes, 2 * payloadSize, messageBytes.length)); + + startProcessing(); + mBluetoothGattServerCallback.onCharacteristicWriteRequest( + mMockBluetoothDevice, + /*requestId=*/ 4, + IN_CHARACTERSTIC, + /*preparedWrite=*/ false, + /*responseNeeded=*/ true, + /*offset=*/ 0, + dataPacket1.toBytes()); + mBluetoothGattServerCallback.onCharacteristicWriteRequest( + mMockBluetoothDevice, + /*requestId=*/ 4, + IN_CHARACTERSTIC, + /*preparedWrite=*/ false, + /*responseNeeded=*/ true, + /*offset=*/ 0, + dataPacket2.toBytes()); + mBluetoothGattServerCallback.onCharacteristicWriteRequest( + mMockBluetoothDevice, + /*requestId=*/ 4, + IN_CHARACTERSTIC, + /*preparedWrite=*/ false, + /*responseNeeded=*/ true, + /*offset=*/ 0, + dataPacket3.toBytes()); + + verify(mMockBluetoothGattServer, times(3)) + .sendResponse( + eq(mMockBluetoothDevice), + eq(/*requestId=*/ 4), + eq(BluetoothGatt.GATT_SUCCESS), + eq(/*offset=*/ 0), + any()); + verify(mMockBluetoothGattServer, times(1)) + .sendResponse( + mMockBluetoothDevice, + /*requestId=*/ 4, + BluetoothGatt.GATT_SUCCESS, + /*offset=*/ 0, + dataPacket1.toBytes()); + verify(mMockBluetoothGattServer, times(1)) + .sendResponse( + mMockBluetoothDevice, + /*requestId=*/ 4, + BluetoothGatt.GATT_SUCCESS, + /*offset=*/ 0, + dataPacket2.toBytes()); + verify(mMockBluetoothGattServer, times(1)) + .sendResponse( + mMockBluetoothDevice, + /*requestId=*/ 4, + BluetoothGatt.GATT_SUCCESS, + /*offset=*/ 0, + dataPacket3.toBytes()); + ArgumentCaptor<FiraConnectorMessage> captor = + ArgumentCaptor.forClass(FiraConnectorMessage.class); + verify(mMockTransportServerCallback, times(1)).onMessage(eq(SECID), captor.capture()); + assertThat(captor.getValue().toString()).isEqualTo(message.toString()); + } +} diff --git a/service/tests/src/com/android/server/uwb/params/FiraDecoderTest.java b/service/tests/src/com/android/server/uwb/params/FiraDecoderTest.java index 44810674..6e1f9762 100644 --- a/service/tests/src/com/android/server/uwb/params/FiraDecoderTest.java +++ b/service/tests/src/com/android/server/uwb/params/FiraDecoderTest.java @@ -87,10 +87,11 @@ public class FiraDecoderTest { + "0F050000000003" + "10010F" + "110101" - + "E30101"; + + "E30101" + + "E40401010101"; private static final byte[] TEST_FIRA_SPECIFICATION_TLV_DATA = UwbUtil.getByteArray(TEST_FIRA_SPECIFICATION_TLV_STRING); - public static final int TEST_FIRA_SPECIFICATION_TLV_NUM_PARAMS = 19; + public static final int TEST_FIRA_SPECIFICATION_TLV_NUM_PARAMS = 20; private final FiraDecoder mFiraDecoder = new FiraDecoder(); public static void verifyFiraSpecification(FiraSpecificationParams firaSpecificationParams) { |