diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..dadba56 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space + +[*.{java,kt,kts}] +indent_size = 4 + +[*.{xml,yml,yaml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..7d77807 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,45 @@ +name: build + +on: + pull_request: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out source with submodules + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Assemble debug APK + run: ./gradlew :app:assembleDebug --no-daemon + + - name: Run Android lint + run: ./gradlew :app:lintDebug --no-daemon + + - name: Run unit tests + run: ./gradlew :app:testDebugUnitTest --no-daemon + + - name: Upload reports on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: reports + path: | + app/build/reports/ + JavaSake/lib/build/reports/ + retention-days: 7 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4b94742 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "JavaSake"] + path = JavaSake + url = https://github.com/OpenMinimed/JavaSake.git diff --git a/JavaSake b/JavaSake new file mode 160000 index 0000000..07dc6d6 --- /dev/null +++ b/JavaSake @@ -0,0 +1 @@ +Subproject commit 07dc6d69aec0a246027cffafab991a280cc7aa52 diff --git a/README.md b/README.md index 07f62dc..b166c8e 100644 --- a/README.md +++ b/README.md @@ -3,5 +3,5 @@ ### TODO: - [x] rename from me.palmarci to org.openminimed -- [ ] implement SAKE handshake +- [x] implement SAKE handshake diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cb4f18b..87b923f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.android.application) + alias(libs.plugins.spotless) } android { @@ -31,9 +32,29 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } + + lint { + baseline = file("lint-baseline.xml") + warningsAsErrors = false + } +} + +spotless { + java { + target("src/**/*.java") + googleJavaFormat(libs.versions.googleJavaFormat.get()).aosp() + removeUnusedImports() + trimTrailingWhitespace() + endWithNewline() + } } dependencies { + // Placeholder coordinates for the JavaSake :lib subproject. Gradle substitutes the local + // composite build (declared in settings.gradle.kts) at configuration time, so this never + // actually resolves against a Maven repository. + implementation("org.openminimed:lib:0.1.0-SNAPSHOT") + implementation(libs.appcompat) implementation(libs.material) implementation(libs.activity) diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml new file mode 100644 index 0000000..25adf4e --- /dev/null +++ b/app/lint-baseline.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/androidTest/java/org/openminimed/pumpconnector/ExampleInstrumentedTest.java b/app/src/androidTest/java/org/openminimed/pumpconnector/ExampleInstrumentedTest.java index 0eaaffa..6c4b0c5 100644 --- a/app/src/androidTest/java/org/openminimed/pumpconnector/ExampleInstrumentedTest.java +++ b/app/src/androidTest/java/org/openminimed/pumpconnector/ExampleInstrumentedTest.java @@ -1,15 +1,13 @@ package org.openminimed.pumpconnector; -import android.content.Context; +import static org.junit.Assert.*; -import androidx.test.platform.app.InstrumentationRegistry; +import android.content.Context; import androidx.test.ext.junit.runners.AndroidJUnit4; - +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; -import static org.junit.Assert.*; - /** * Instrumented test, which will execute on an Android device. * @@ -23,4 +21,4 @@ public void useAppContext() { Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); assertEquals("org.openminimed.pumpconnector", appContext.getPackageName()); } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/openminimed/pumpconnector/BlePeripheralDevice.java b/app/src/main/java/org/openminimed/pumpconnector/BlePeripheralDevice.java index 589ba12..f82b32d 100644 --- a/app/src/main/java/org/openminimed/pumpconnector/BlePeripheralDevice.java +++ b/app/src/main/java/org/openminimed/pumpconnector/BlePeripheralDevice.java @@ -16,41 +16,49 @@ import android.bluetooth.le.BluetoothLeAdvertiser; import android.content.Context; import android.content.pm.PackageManager; -import androidx.appcompat.app.AppCompatActivity; import android.os.ParcelUuid; import android.util.Log; - +import androidx.appcompat.app.AppCompatActivity; import java.nio.charset.StandardCharsets; -import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.UUID; -/** - * BLE Peripheral Device implementation with custom advertisement and services - */ +/** BLE Peripheral Device implementation with custom advertisement and services */ public class BlePeripheralDevice { private static final String TAG = "BlePeripheralDevice"; // Custom UUIDs - private static final UUID DEVICE_INFO_SERVICE_UUID = UUID.fromString("00000900-0000-1000-0000-009132591325"); - private static final UUID SAKE_SERVICE_UUID = UUID.fromString("0000fe82-0000-1000-8000-00805f9b34fb"); - private static final UUID SAKE_CHARACTERISTIC_UUID = UUID.fromString("0000fe82-0000-1000-0000-009132591325"); + private static final UUID DEVICE_INFO_SERVICE_UUID = + UUID.fromString("00000900-0000-1000-0000-009132591325"); + private static final UUID SAKE_SERVICE_UUID = + UUID.fromString("0000fe82-0000-1000-8000-00805f9b34fb"); + private static final UUID SAKE_CHARACTERISTIC_UUID = + UUID.fromString("0000fe82-0000-1000-0000-009132591325"); // Standard Device Information Service characteristics (16-bit UUIDs) - private static final UUID MANUFACTURER_NAME_UUID = UUID.fromString("00002a29-0000-1000-8000-00805f9b34fb"); - private static final UUID MODEL_NUMBER_UUID = UUID.fromString("00002a24-0000-1000-8000-00805f9b34fb"); - private static final UUID SERIAL_NUMBER_UUID = UUID.fromString("00002a25-0000-1000-8000-00805f9b34fb"); - private static final UUID HARDWARE_REVISION_UUID = UUID.fromString("00002a27-0000-1000-8000-00805f9b34fb"); - private static final UUID FIRMWARE_REVISION_UUID = UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb"); - private static final UUID SOFTWARE_REVISION_UUID = UUID.fromString("00002a28-0000-1000-8000-00805f9b34fb"); - private static final UUID SYSTEM_ID_UUID = UUID.fromString("00002a23-0000-1000-8000-00805f9b34fb"); + private static final UUID MANUFACTURER_NAME_UUID = + UUID.fromString("00002a29-0000-1000-8000-00805f9b34fb"); + private static final UUID MODEL_NUMBER_UUID = + UUID.fromString("00002a24-0000-1000-8000-00805f9b34fb"); + private static final UUID SERIAL_NUMBER_UUID = + UUID.fromString("00002a25-0000-1000-8000-00805f9b34fb"); + private static final UUID HARDWARE_REVISION_UUID = + UUID.fromString("00002a27-0000-1000-8000-00805f9b34fb"); + private static final UUID FIRMWARE_REVISION_UUID = + UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb"); + private static final UUID SOFTWARE_REVISION_UUID = + UUID.fromString("00002a28-0000-1000-8000-00805f9b34fb"); + private static final UUID SYSTEM_ID_UUID = + UUID.fromString("00002a23-0000-1000-8000-00805f9b34fb"); private static final UUID PNP_ID_UUID = UUID.fromString("00002a50-0000-1000-8000-00805f9b34fb"); - private static final UUID REGULATORY_CERT_UUID = UUID.fromString("00002a2a-0000-1000-8000-00805f9b34fb"); + private static final UUID REGULATORY_CERT_UUID = + UUID.fromString("00002a2a-0000-1000-8000-00805f9b34fb"); // Client Characteristic Configuration descriptor - private static final UUID CCC_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); + private static final UUID CCC_DESCRIPTOR_UUID = + UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); private final Context context; private BluetoothManager bluetoothManager; @@ -58,17 +66,23 @@ public class BlePeripheralDevice { private BluetoothLeAdvertiser advertiser; private BluetoothGattServer gattServer; private Queue addServiceQueue; + private BluetoothGattCharacteristic sakeCharacteristic; + private AdvertiseCallback advertiseCallback; + private final SakeHandler sakeHandler = new SakeHandler(); // Permission check method public boolean hasBluetoothPermissions() { // For API 31+ (Android 12+), we need BLUETOOTH_CONNECT and BLUETOOTH_ADVERTISE if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { - return context.checkSelfPermission(android.Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED && - context.checkSelfPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE) == PackageManager.PERMISSION_GRANTED; + return context.checkSelfPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + == PackageManager.PERMISSION_GRANTED + && context.checkSelfPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE) + == PackageManager.PERMISSION_GRANTED; } // For API 23-30 (Android 6.0 - 11), we need ACCESS_FINE_LOCATION for Bluetooth operations else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - return context.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; + return context.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) + == PackageManager.PERMISSION_GRANTED; } // For API < 23, no runtime permissions needed else { @@ -93,15 +107,16 @@ public void requestBluetoothPermissions() { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { // For Android 12+ - activity.requestPermissions(new String[]{ - android.Manifest.permission.BLUETOOTH_CONNECT, - android.Manifest.permission.BLUETOOTH_ADVERTISE - }, 101); + activity.requestPermissions( + new String[] { + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.BLUETOOTH_ADVERTISE + }, + 101); } else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { // For Android 6.0 - 11 - activity.requestPermissions(new String[]{ - android.Manifest.permission.ACCESS_FINE_LOCATION - }, 101); + activity.requestPermissions( + new String[] {android.Manifest.permission.ACCESS_FINE_LOCATION}, 101); } // For Android < 6.0, no runtime permissions needed } @@ -154,29 +169,31 @@ private void startAdvertising() { } // Create advertisement data - AdvertiseSettings settings = new AdvertiseSettings.Builder() - .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED) - .setConnectable(true) - .setTimeout(0) // Advertise until explicitly stopped - .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) - .build(); + AdvertiseSettings settings = + new AdvertiseSettings.Builder() + .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED) + .setConnectable(true) + .setTimeout(0) // Advertise until explicitly stopped + .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) + .build(); AdvertiseData data = createAdvertisementData(); - AdvertiseCallback callback = new AdvertiseCallback() { - @Override - public void onStartSuccess(AdvertiseSettings settingsInEffect) { - Log.i(TAG, "Advertising started successfully"); - Log.d(TAG, "Advertise settings: " + settingsInEffect); - } + advertiseCallback = + new AdvertiseCallback() { + @Override + public void onStartSuccess(AdvertiseSettings settingsInEffect) { + Log.i(TAG, "Advertising started successfully"); + Log.d(TAG, "Advertise settings: " + settingsInEffect); + } - @Override - public void onStartFailure(int errorCode) { - Log.e(TAG, "Advertising failed with error: " + errorCode); - } - }; + @Override + public void onStartFailure(int errorCode) { + Log.e(TAG, "Advertising failed with error: " + errorCode); + } + }; - advertiser.startAdvertising(settings, data, callback); + advertiser.startAdvertising(settings, data, advertiseCallback); Log.d(TAG, "Advertising started"); } @@ -194,7 +211,7 @@ private AdvertiseData createAdvertisementData() { // Add service data for the custom service byte[] serviceData = new byte[] {0x01}; // Simple service data - // builder.addServiceData(serviceUuid, serviceData); + // builder.addServiceData(serviceUuid, serviceData); return builder.build(); } @@ -238,119 +255,131 @@ private void startGattServer() { } private BluetoothGattService createDeviceInfoService() { - BluetoothGattService service = new BluetoothGattService( - DEVICE_INFO_SERVICE_UUID, - BluetoothGattService.SERVICE_TYPE_PRIMARY - ); + BluetoothGattService service = + new BluetoothGattService( + DEVICE_INFO_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY); // Manufacturer Name String [R] - BluetoothGattCharacteristic manufacturerName = new BluetoothGattCharacteristic( - MANUFACTURER_NAME_UUID, - BluetoothGattCharacteristic.PROPERTY_READ, - BluetoothGattCharacteristic.PERMISSION_READ - ); + BluetoothGattCharacteristic manufacturerName = + new BluetoothGattCharacteristic( + MANUFACTURER_NAME_UUID, + BluetoothGattCharacteristic.PROPERTY_READ, + BluetoothGattCharacteristic.PERMISSION_READ); manufacturerName.setValue("Test Manufacturer"); service.addCharacteristic(manufacturerName); // Model Number String [R] - BluetoothGattCharacteristic modelNumber = new BluetoothGattCharacteristic( - MODEL_NUMBER_UUID, - BluetoothGattCharacteristic.PROPERTY_READ, - BluetoothGattCharacteristic.PERMISSION_READ - ); + BluetoothGattCharacteristic modelNumber = + new BluetoothGattCharacteristic( + MODEL_NUMBER_UUID, + BluetoothGattCharacteristic.PROPERTY_READ, + BluetoothGattCharacteristic.PERMISSION_READ); modelNumber.setValue("Test Model Number"); service.addCharacteristic(modelNumber); // Serial Number String [R] - BluetoothGattCharacteristic serialNumber = new BluetoothGattCharacteristic( - SERIAL_NUMBER_UUID, - BluetoothGattCharacteristic.PROPERTY_READ, - BluetoothGattCharacteristic.PERMISSION_READ - ); + BluetoothGattCharacteristic serialNumber = + new BluetoothGattCharacteristic( + SERIAL_NUMBER_UUID, + BluetoothGattCharacteristic.PROPERTY_READ, + BluetoothGattCharacteristic.PERMISSION_READ); serialNumber.setValue(MOBILE_NAME); service.addCharacteristic(serialNumber); // Hardware Revision String [R] - BluetoothGattCharacteristic hardwareRevision = new BluetoothGattCharacteristic( - HARDWARE_REVISION_UUID, - BluetoothGattCharacteristic.PROPERTY_READ, - BluetoothGattCharacteristic.PERMISSION_READ - ); + BluetoothGattCharacteristic hardwareRevision = + new BluetoothGattCharacteristic( + HARDWARE_REVISION_UUID, + BluetoothGattCharacteristic.PROPERTY_READ, + BluetoothGattCharacteristic.PERMISSION_READ); hardwareRevision.setValue("HW-1.0"); service.addCharacteristic(hardwareRevision); // Firmware Revision String [R] - BluetoothGattCharacteristic firmwareRevision = new BluetoothGattCharacteristic( - FIRMWARE_REVISION_UUID, - BluetoothGattCharacteristic.PROPERTY_READ, - BluetoothGattCharacteristic.PERMISSION_READ - ); + BluetoothGattCharacteristic firmwareRevision = + new BluetoothGattCharacteristic( + FIRMWARE_REVISION_UUID, + BluetoothGattCharacteristic.PROPERTY_READ, + BluetoothGattCharacteristic.PERMISSION_READ); firmwareRevision.setValue("14587043"); service.addCharacteristic(firmwareRevision); // Software Revision String [R] - BluetoothGattCharacteristic softwareRevision = new BluetoothGattCharacteristic( - SOFTWARE_REVISION_UUID, - BluetoothGattCharacteristic.PROPERTY_READ, - BluetoothGattCharacteristic.PERMISSION_READ - ); - // softwareRevision.setValue("SW-3.2.1"); + BluetoothGattCharacteristic softwareRevision = + new BluetoothGattCharacteristic( + SOFTWARE_REVISION_UUID, + BluetoothGattCharacteristic.PROPERTY_READ, + BluetoothGattCharacteristic.PERMISSION_READ); + // softwareRevision.setValue("SW-3.2.1"); softwareRevision.setValue(FAKE_APP_VER); // APK version with git commit? service.addCharacteristic(softwareRevision); // System ID [R] - BluetoothGattCharacteristic systemId = new BluetoothGattCharacteristic( - SYSTEM_ID_UUID, - BluetoothGattCharacteristic.PROPERTY_READ, - BluetoothGattCharacteristic.PERMISSION_READ - ); - systemId.setValue(new byte[]{(byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte)0x0, (byte) 0x0, (byte) 0x0}); + BluetoothGattCharacteristic systemId = + new BluetoothGattCharacteristic( + SYSTEM_ID_UUID, + BluetoothGattCharacteristic.PROPERTY_READ, + BluetoothGattCharacteristic.PERMISSION_READ); + systemId.setValue( + new byte[] { + (byte) 0x0, + (byte) 0x0, + (byte) 0x0, + (byte) 0x0, + (byte) 0x0, + (byte) 0x0, + (byte) 0x0, + (byte) 0x0 + }); service.addCharacteristic(systemId); // PnP ID [R] - BluetoothGattCharacteristic pnpId = new BluetoothGattCharacteristic( - PNP_ID_UUID, - BluetoothGattCharacteristic.PROPERTY_READ, - BluetoothGattCharacteristic.PERMISSION_READ - ); - pnpId.setValue(new byte[]{0x0}); + BluetoothGattCharacteristic pnpId = + new BluetoothGattCharacteristic( + PNP_ID_UUID, + BluetoothGattCharacteristic.PROPERTY_READ, + BluetoothGattCharacteristic.PERMISSION_READ); + pnpId.setValue(new byte[] {0x0}); service.addCharacteristic(pnpId); // IEEE 11073-20601 Regulatory Certification Data List [R] - BluetoothGattCharacteristic regulatoryCert = new BluetoothGattCharacteristic( - REGULATORY_CERT_UUID, - BluetoothGattCharacteristic.PROPERTY_READ, - BluetoothGattCharacteristic.PERMISSION_READ - ); - regulatoryCert.setValue(new byte[]{}); + BluetoothGattCharacteristic regulatoryCert = + new BluetoothGattCharacteristic( + REGULATORY_CERT_UUID, + BluetoothGattCharacteristic.PROPERTY_READ, + BluetoothGattCharacteristic.PERMISSION_READ); + regulatoryCert.setValue(new byte[] {}); service.addCharacteristic(regulatoryCert); return service; } private BluetoothGattService createSakeService() { - BluetoothGattService service = new BluetoothGattService( - SAKE_SERVICE_UUID, - BluetoothGattService.SERVICE_TYPE_PRIMARY - ); + BluetoothGattService service = + new BluetoothGattService( + SAKE_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY); // Unknown Characteristic [N W] - BluetoothGattCharacteristic sakeChar = new BluetoothGattCharacteristic( - SAKE_CHARACTERISTIC_UUID, - BluetoothGattCharacteristic.PROPERTY_NOTIFY | BluetoothGattCharacteristic.PROPERTY_WRITE, - BluetoothGattCharacteristic.PERMISSION_WRITE - ); + BluetoothGattCharacteristic sakeChar = + new BluetoothGattCharacteristic( + SAKE_CHARACTERISTIC_UUID, + BluetoothGattCharacteristic.PROPERTY_NOTIFY + | BluetoothGattCharacteristic.PROPERTY_WRITE, + BluetoothGattCharacteristic.PERMISSION_WRITE); // Add Client Characteristic Configuration descriptor - BluetoothGattDescriptor cccDescriptor = new BluetoothGattDescriptor( - CCC_DESCRIPTOR_UUID, - BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE - ); + BluetoothGattDescriptor cccDescriptor = + new BluetoothGattDescriptor( + CCC_DESCRIPTOR_UUID, + BluetoothGattDescriptor.PERMISSION_READ + | BluetoothGattDescriptor.PERMISSION_WRITE); sakeChar.addDescriptor(cccDescriptor); service.addCharacteristic(sakeChar); + this.sakeCharacteristic = sakeChar; + return service; } @@ -366,6 +395,7 @@ private void addNextService() { Log.d(TAG, "All services added successfully"); Log.d(TAG, "Device Info Service UUID: " + DEVICE_INFO_SERVICE_UUID); Log.d(TAG, "SAKE Service UUID: " + SAKE_SERVICE_UUID); + sakeHandler.attach(gattServer, sakeCharacteristic); } else { BluetoothGattService firstItem = addServiceQueue.poll(); if (firstItem != null) { @@ -380,18 +410,17 @@ private void addNextService() { } private void stopAdvertising() { - if (advertiser != null) { - try { - advertiser.stopAdvertising(new AdvertiseCallback() { - @Override - public void onStartSuccess(AdvertiseSettings settingsInEffect) { - // Not expected for stop - } - }); - Log.d(TAG, "Advertising stopped"); - } catch (SecurityException e) { - Log.e(TAG, "Security exception when stopping advertising: " + e.getMessage()); - } + if (advertiser == null || advertiseCallback == null) { + return; + } + try { + // Must pass the same callback instance startAdvertising() used. + advertiser.stopAdvertising(advertiseCallback); + Log.d(TAG, "Advertising stopped"); + } catch (SecurityException e) { + Log.e(TAG, "Security exception when stopping advertising: " + e.getMessage()); + } finally { + advertiseCallback = null; } } @@ -408,189 +437,311 @@ private void stopGattServer() { } // GATT Server Callback - private final BluetoothGattServerCallback gattServerCallback = new BluetoothGattServerCallback() { - @Override - public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { - super.onConnectionStateChange(device, status, newState); - - String deviceAddress = device != null ? device.getAddress() : "unknown"; - - if (newState == BluetoothProfile.STATE_CONNECTED) { - Log.i(TAG, "Device connected: " + deviceAddress); - Log.d(TAG, "Connection status: " + status); - - // Stop advertising when connected - stopAdvertising(); - - // Set security requirements - No Input No Output (Just Works) - /* - if (device != null) { - device.setPairingConfirmation(true); - Log.d(TAG, "Pairing confirmation set to true for: " + deviceAddress); + private final BluetoothGattServerCallback gattServerCallback = + new BluetoothGattServerCallback() { + @Override + public void onConnectionStateChange( + BluetoothDevice device, int status, int newState) { + super.onConnectionStateChange(device, status, newState); + + String deviceAddress = device != null ? device.getAddress() : "unknown"; + + if (newState == BluetoothProfile.STATE_CONNECTED) { + Log.i(TAG, "Device connected: " + deviceAddress); + Log.d(TAG, "Connection status: " + status); + + // Stop advertising when connected + stopAdvertising(); + + // Set security requirements - No Input No Output (Just Works) + /* + if (device != null) { + device.setPairingConfirmation(true); + Log.d(TAG, "Pairing confirmation set to true for: " + deviceAddress); + } + */ + + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + Log.i(TAG, "Device disconnected: " + deviceAddress); + Log.d(TAG, "Disconnection status: " + status); + + // Restart advertising when disconnected + startAdvertising(); + } } - */ - - } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { - Log.i(TAG, "Device disconnected: " + deviceAddress); - Log.d(TAG, "Disconnection status: " + status); - - // Restart advertising when disconnected - startAdvertising(); - } - } - - @Override - public void onServiceAdded(int status, BluetoothGattService service) { - super.onServiceAdded(status, service); - Log.d(TAG, "Service added: " + service.getUuid() + " status: " + status); - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.d(TAG, "Service added successfully: " + service.getUuid()); - addNextService(); - } else { - Log.e(TAG, "Failed to add service: " + service.getUuid() + " with status: " + status); - } - } - - @Override - public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) { - super.onCharacteristicReadRequest(device, requestId, offset, characteristic); - - var uuid = characteristic.getUuid(); - Log.i(TAG, "Read request from: " + device.getAddress()); - Log.d(TAG, "Characteristic UUID: " + uuid); - Log.d(TAG, "Request ID: " + requestId + ", Offset: " + offset); - - try { - byte[] toSend = "DummyData".getBytes(StandardCharsets.UTF_8); - - if (uuid.equals(SOFTWARE_REVISION_UUID)) { - toSend = FAKE_APP_VER.getBytes(StandardCharsets.UTF_8); // might not be necessary? + @Override + public void onServiceAdded(int status, BluetoothGattService service) { + super.onServiceAdded(status, service); + Log.d(TAG, "Service added: " + service.getUuid() + " status: " + status); + + if (status == BluetoothGatt.GATT_SUCCESS) { + Log.d(TAG, "Service added successfully: " + service.getUuid()); + addNextService(); + } else { + Log.e( + TAG, + "Failed to add service: " + + service.getUuid() + + " with status: " + + status); + } } - if (uuid.equals(SYSTEM_ID_UUID)) { - toSend = new byte[]{0, 0, 0, 0,0, 0, 0, 0}; + @Override + public void onCharacteristicReadRequest( + BluetoothDevice device, + int requestId, + int offset, + BluetoothGattCharacteristic characteristic) { + super.onCharacteristicReadRequest(device, requestId, offset, characteristic); + + var uuid = characteristic.getUuid(); + Log.i(TAG, "Read request from: " + device.getAddress()); + Log.d(TAG, "Characteristic UUID: " + uuid); + Log.d(TAG, "Request ID: " + requestId + ", Offset: " + offset); + + try { + byte[] stored = characteristic.getValue(); + byte[] toSend = + stored != null + ? stored + : "DummyData".getBytes(StandardCharsets.UTF_8); + + if (uuid.equals(SOFTWARE_REVISION_UUID)) { + toSend = + FAKE_APP_VER.getBytes( + StandardCharsets.UTF_8); // might not be necessary? + } + + if (uuid.equals(SYSTEM_ID_UUID)) { + toSend = new byte[] {0, 0, 0, 0, 0, 0, 0, 0}; + } + + if (uuid.equals(PNP_ID_UUID)) { + // https://btprodspecificationrefs.blob.core.windows.net/gatt-specification-supplement/GATT_Specification_Supplement.pdf + // vid source, vendor id, product id, product version, + toSend = + new byte[] { + 0, 0, 0, 0, 0, 0, 0, + }; + } + gattServer.sendResponse( + device, requestId, BluetoothGatt.GATT_SUCCESS, offset, toSend); + + Log.d(TAG, "Sent response: 0x " + bytesToHex(toSend)); + } catch (SecurityException e) { + Log.e(TAG, "Security exception in read request: " + e.getMessage()); + } } - if (uuid.equals(PNP_ID_UUID)) { - // https://btprodspecificationrefs.blob.core.windows.net/gatt-specification-supplement/GATT_Specification_Supplement.pdf - // vid source, vendor id, product id, product version, - toSend = new byte[]{0, 0, 0, 0,0, 0, 0, }; + @Override + public void onCharacteristicWriteRequest( + BluetoothDevice device, + int requestId, + BluetoothGattCharacteristic characteristic, + boolean preparedWrite, + boolean responseNeeded, + int offset, + byte[] value) { + super.onCharacteristicWriteRequest( + device, + requestId, + characteristic, + preparedWrite, + responseNeeded, + offset, + value); + + Log.i(TAG, "Write request from: " + device.getAddress()); + Log.d(TAG, "Characteristic UUID: " + characteristic.getUuid()); + Log.d(TAG, "Value: " + bytesToHex(value)); + Log.d( + TAG, + "Prepared write: " + + preparedWrite + + ", Response needed: " + + responseNeeded); + + if (characteristic.getUuid().equals(SAKE_CHARACTERISTIC_UUID)) { + sakeHandler.onWrite(value); + } + if (responseNeeded) { + try { + gattServer.sendResponse( + device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value); + } catch (SecurityException e) { + Log.e(TAG, "Security exception in write request: " + e.getMessage()); + } + } } - gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, toSend); - - Log.d(TAG, "Sent response: 0x " + bytesToHex(toSend)); - } catch (SecurityException e) { - Log.e(TAG, "Security exception in read request: " + e.getMessage()); - } - } - - @Override - public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { - super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value); - Log.i(TAG, "Write request from: " + device.getAddress()); - Log.d(TAG, "Characteristic UUID: " + characteristic.getUuid()); - Log.d(TAG, "Value: " + bytesToHex(value)); - Log.d(TAG, "Prepared write: " + preparedWrite + ", Response needed: " + responseNeeded); - - if (responseNeeded) { - try { - gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value); - } catch (SecurityException e) { - Log.e(TAG, "Security exception in write request: " + e.getMessage()); + @Override + public void onDescriptorReadRequest( + BluetoothDevice device, + int requestId, + int offset, + BluetoothGattDescriptor descriptor) { + super.onDescriptorReadRequest(device, requestId, offset, descriptor); + + Log.i(TAG, "Descriptor read request from: " + device.getAddress()); + Log.d(TAG, "Descriptor UUID: " + descriptor.getUuid()); + + try { + // For CCC descriptor, return current value (0 by default) + byte[] value = descriptor.getValue(); + if (value == null) { + value = new byte[] {0x00, 0x00}; // Default CCC value + } + gattServer.sendResponse( + device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value); + + Log.d(TAG, "Sent descriptor value: " + bytesToHex(value)); + } catch (SecurityException e) { + Log.e( + TAG, + "Security exception in descriptor read request: " + e.getMessage()); + } } - } - } - - @Override - public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) { - super.onDescriptorReadRequest(device, requestId, offset, descriptor); - Log.i(TAG, "Descriptor read request from: " + device.getAddress()); - Log.d(TAG, "Descriptor UUID: " + descriptor.getUuid()); + @Override + public void onDescriptorWriteRequest( + BluetoothDevice device, + int requestId, + BluetoothGattDescriptor descriptor, + boolean preparedWrite, + boolean responseNeeded, + int offset, + byte[] value) { + super.onDescriptorWriteRequest( + device, + requestId, + descriptor, + preparedWrite, + responseNeeded, + offset, + value); + + Log.i(TAG, "Descriptor write request from: " + device.getAddress()); + Log.d(TAG, "Descriptor UUID: " + descriptor.getUuid()); + Log.d(TAG, "Value: " + bytesToHex(value)); + + if (descriptor.getUuid().equals(CCC_DESCRIPTOR_UUID)) { + if (value == null || value.length < 2) { + Log.w(TAG, "CCC write with malformed value, ignoring"); + if (responseNeeded) { + try { + gattServer.sendResponse( + device, + requestId, + BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH, + offset, + null); + } catch (SecurityException e) { + Log.e( + TAG, + "Security exception in CCC malformed-value response: " + + e.getMessage()); + } + } + return; + } + int cccValue = ((value[1] & 0xFF) << 8) | (value[0] & 0xFF); + + if ((cccValue & 0x0001) != 0) { + Log.i(TAG, "Client subscribed to NOTIFICATIONS"); + } + if ((cccValue & 0x0002) != 0) { + Log.i(TAG, "Client subscribed to INDICATIONS"); + } + if (cccValue == 0x0000) { + Log.i(TAG, "Client unsubscribed from notifications/indications"); + } + + boolean isSakeChar = + descriptor.getCharacteristic() != null + && SAKE_CHARACTERISTIC_UUID.equals( + descriptor.getCharacteristic().getUuid()); + if (isSakeChar) { + if ((cccValue & 0x0001) != 0) { + sakeHandler.onNotificationsEnabled(device); + } else if (cccValue == 0x0000) { + sakeHandler.onNotificationsDisabled(); + } + } + } - try { - // For CCC descriptor, return current value (0 by default) - byte[] value = descriptor.getValue(); - if (value == null) { - value = new byte[]{0x00, 0x00}; // Default CCC value + // Update descriptor value + descriptor.setValue(value); + + if (responseNeeded) { + try { + gattServer.sendResponse( + device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value); + } catch (SecurityException e) { + Log.e( + TAG, + "Security exception in descriptor write request: " + + e.getMessage()); + } + } } - gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value); - - Log.d(TAG, "Sent descriptor value: " + bytesToHex(value)); - } catch (SecurityException e) { - Log.e(TAG, "Security exception in descriptor read request: " + e.getMessage()); - } - } - - @Override - public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { - super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value); - - Log.i(TAG, "Descriptor write request from: " + device.getAddress()); - Log.d(TAG, "Descriptor UUID: " + descriptor.getUuid()); - Log.d(TAG, "Value: " + bytesToHex(value)); - if (descriptor.getUuid().equals(CCC_DESCRIPTOR_UUID)) { - int cccValue = (value[1] << 8) | (value[0] & 0xFF); - - if ((cccValue & 0x0001) != 0) { - Log.i(TAG, "Client subscribed to NOTIFICATIONS"); - } - if ((cccValue & 0x0002) != 0) { - Log.i(TAG, "Client subscribed to INDICATIONS"); - } - if (cccValue == 0x0000) { - Log.i(TAG, "Client unsubscribed from notifications/indications"); + @Override + public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) { + super.onExecuteWrite(device, requestId, execute); + Log.d( + TAG, + "Execute write from: " + device.getAddress() + ", Execute: " + execute); } - } - - // Update descriptor value - descriptor.setValue(value); - if (responseNeeded) { - try { - gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value); - } catch (SecurityException e) { - Log.e(TAG, "Security exception in descriptor write request: " + e.getMessage()); + @Override + public void onNotificationSent(BluetoothDevice device, int status) { + super.onNotificationSent(device, status); + Log.d( + TAG, + "Notification sent to: " + device.getAddress() + ", Status: " + status); } - } - } - - @Override - public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) { - super.onExecuteWrite(device, requestId, execute); - Log.d(TAG, "Execute write from: " + device.getAddress() + ", Execute: " + execute); - } - @Override - public void onNotificationSent(BluetoothDevice device, int status) { - super.onNotificationSent(device, status); - Log.d(TAG, "Notification sent to: " + device.getAddress() + ", Status: " + status); - } - - @Override - public void onMtuChanged(BluetoothDevice device, int mtu) { - super.onMtuChanged(device, mtu); - Log.i(TAG, "MTU changed for device: " + device.getAddress() + ", New MTU: " + mtu); - } + @Override + public void onMtuChanged(BluetoothDevice device, int mtu) { + super.onMtuChanged(device, mtu); + Log.i( + TAG, + "MTU changed for device: " + device.getAddress() + ", New MTU: " + mtu); + } - @Override - public void onPhyUpdate(BluetoothDevice device, int txPhy, int rxPhy, int status) { - super.onPhyUpdate(device, txPhy, rxPhy, status); - Log.d(TAG, "PHY update for device: " + device.getAddress() + - ", TX PHY: " + txPhy + ", RX PHY: " + rxPhy + ", Status: " + status); - } + @Override + public void onPhyUpdate(BluetoothDevice device, int txPhy, int rxPhy, int status) { + super.onPhyUpdate(device, txPhy, rxPhy, status); + Log.d( + TAG, + "PHY update for device: " + + device.getAddress() + + ", TX PHY: " + + txPhy + + ", RX PHY: " + + rxPhy + + ", Status: " + + status); + } - @Override - public void onPhyRead(BluetoothDevice device, int txPhy, int rxPhy, int status) { - super.onPhyRead(device, txPhy, rxPhy, status); - Log.d(TAG, "PHY read for device: " + device.getAddress() + - ", TX PHY: " + txPhy + ", RX PHY: " + rxPhy + ", Status: " + status); - } - }; + @Override + public void onPhyRead(BluetoothDevice device, int txPhy, int rxPhy, int status) { + super.onPhyRead(device, txPhy, rxPhy, status); + Log.d( + TAG, + "PHY read for device: " + + device.getAddress() + + ", TX PHY: " + + txPhy + + ", RX PHY: " + + rxPhy + + ", Status: " + + status); + } + }; // Utility method to convert bytes to hex string private static String bytesToHex(byte[] bytes) { @@ -606,10 +757,11 @@ private static String bytesToHex(byte[] bytes) { } // Security callback for pairing/bonding - private final BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() { - @Override - public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) { - // Not used in peripheral mode, but kept for reference - } - }; + private final BluetoothAdapter.LeScanCallback leScanCallback = + new BluetoothAdapter.LeScanCallback() { + @Override + public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) { + // Not used in peripheral mode, but kept for reference + } + }; } diff --git a/app/src/main/java/org/openminimed/pumpconnector/MainActivity.java b/app/src/main/java/org/openminimed/pumpconnector/MainActivity.java index 4f00a2f..5fa4553 100644 --- a/app/src/main/java/org/openminimed/pumpconnector/MainActivity.java +++ b/app/src/main/java/org/openminimed/pumpconnector/MainActivity.java @@ -1,17 +1,14 @@ package org.openminimed.pumpconnector; import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; import androidx.activity.EdgeToEdge; import androidx.appcompat.app.AppCompatActivity; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; -import android.view.View; -import android.widget.Button; -import android.widget.Toast; - -import org.openminimed.pumpconnector.BlePeripheralDevice; - public class MainActivity extends AppCompatActivity implements View.OnClickListener { @@ -22,17 +19,19 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EdgeToEdge.enable(this); setContentView(R.layout.activity_main); - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { - Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); - return insets; - }); + ViewCompat.setOnApplyWindowInsetsListener( + findViewById(R.id.main), + (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding( + systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); this.ble = new BlePeripheralDevice(MainActivity.this); Button btn = (Button) findViewById(R.id.start_gatt); btn.setOnClickListener(MainActivity.this); - } @Override @@ -45,7 +44,5 @@ public void onClick(View v) { this.ble.start(); Toast.makeText(this, "Peripheral started!", Toast.LENGTH_SHORT).show(); } - } - } diff --git a/app/src/main/java/org/openminimed/pumpconnector/SakeHandler.java b/app/src/main/java/org/openminimed/pumpconnector/SakeHandler.java new file mode 100644 index 0000000..6476c5b --- /dev/null +++ b/app/src/main/java/org/openminimed/pumpconnector/SakeHandler.java @@ -0,0 +1,179 @@ +package org.openminimed.pumpconnector; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattServer; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; +import java.util.Objects; +import org.openminimed.sake.Constants; +import org.openminimed.sake.SakeServer; + +/** + * BLE glue layer for the SAKE handshake. + * + *

Owns a {@link SakeServer} pinned to the extracted-from-pump key database and translates + * between BLE GATT server events and handshake step calls. All SAKE work is serialized onto a + * dedicated {@link HandlerThread} so the binder thread that fires BLE callbacks is never blocked by + * crypto work. + * + *

Wire protocol the pump expects, in order: + * + *

    + *
  1. Pump subscribes to notifications on the SAKE characteristic. Handler emits twenty zero + * bytes as a wake-up notification. + *
  2. Pump writes its own twenty zero bytes as the matching wake-up. Handler feeds that to {@link + * SakeServer#handshake(byte[])}, gets msg0 back, notifies it to the pump. + *
  3. Pump writes msg1, msg3, msg5 in turn. Handler responds with msg2, msg4, and finally + * completes at stage 6. + *
+ * + *

The handler is reusable across pump disconnect/reconnect cycles. A re-subscribe before + * completion is treated as an abort: the {@link SakeServer} is rebuilt from scratch and the + * handshake starts fresh with a new wake-up frame. + */ +public final class SakeHandler { + + private static final String TAG = "SakeHandler"; + private static final int HANDSHAKE_COMPLETE_STAGE = 6; + private static final byte[] WAKE_UP = new byte[20]; + + private final HandlerThread thread; + private final Handler handler; + + private SakeServer server; + private BluetoothGattServer gattServer; + private BluetoothGattCharacteristic characteristic; + private BluetoothDevice peer; + private boolean pumpSubscribed; + + private volatile boolean handshakeComplete; + private volatile boolean closed; + + public SakeHandler() { + this.server = new SakeServer(Constants.KEYDB_PUMP_EXTRACTED); + this.thread = new HandlerThread("sake-handler"); + this.thread.start(); + this.handler = new Handler(this.thread.getLooper()); + } + + /** + * Bind the GATT server and characteristic the handler will use to send SAKE notifications back + * to the pump. Must be called before any of the {@code on*} hooks. + */ + public void attach(BluetoothGattServer gattServer, BluetoothGattCharacteristic characteristic) { + Objects.requireNonNull(gattServer, "gattServer"); + Objects.requireNonNull(characteristic, "characteristic"); + this.gattServer = gattServer; + this.characteristic = characteristic; + } + + /** + * Called from the GATT server callback when the pump subscribes to notifications on the SAKE + * characteristic. Emits a 20-byte wake-up frame. + * + *

If the handshake has not completed and the pump resubscribes (e.g. after a transient + * disconnect), the {@link SakeServer} is reset so the new wake-up starts a fresh handshake. + */ + public void onNotificationsEnabled(BluetoothDevice device) { + if (closed) { + return; + } + handler.post( + () -> { + peer = device; + if (pumpSubscribed) { + return; + } + if (!handshakeComplete && server.getStage() != 0) { + Log.w(TAG, "Pump resubscribed mid-handshake; restarting SAKE state"); + server = new SakeServer(Constants.KEYDB_PUMP_EXTRACTED); + } + pumpSubscribed = true; + Log.i(TAG, "Pump subscribed to SAKE notifications; sending wake-up"); + sendNotification(WAKE_UP.clone()); + }); + } + + /** Called from the GATT server callback when the pump unsubscribes. */ + public void onNotificationsDisabled() { + if (closed) { + return; + } + handler.post( + () -> { + pumpSubscribed = false; + Log.w(TAG, "Pump unsubscribed from SAKE notifications"); + }); + } + + /** + * Called from the GATT server callback for every write on the SAKE characteristic. Drives the + * next handshake step and emits the response. + */ + public void onWrite(byte[] value) { + if (closed) { + return; + } + byte[] copy = value.clone(); + handler.post( + () -> { + if (handshakeComplete) { + Log.w(TAG, "Ignoring write after handshake completion"); + return; + } + try { + byte[] response = server.handshake(copy); + if (response != null) { + sendNotification(response); + } else { + handshakeComplete = true; + Log.i(TAG, "SAKE handshake complete"); + } + } catch (Exception e) { + Log.e(TAG, "SAKE handshake failed", e); + } + }); + } + + /** + * @return true once the handshake has reached stage 6. + */ + public boolean isHandshakeComplete() { + return handshakeComplete; + } + + /** Stop the worker thread and release resources. Subsequent {@code on*} calls become no-ops. */ + public void close() { + closed = true; + handler.removeCallbacksAndMessages(null); + handler.post( + () -> { + gattServer = null; + characteristic = null; + peer = null; + }); + thread.quitSafely(); + } + + @SuppressWarnings("deprecation") + private void sendNotification(byte[] data) { + if (gattServer == null || characteristic == null || peer == null) { + Log.e(TAG, "Cannot send: handler is not attached or has no peer"); + return; + } + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + gattServer.notifyCharacteristicChanged(peer, characteristic, false, data); + } else { + characteristic.setValue(data); + gattServer.notifyCharacteristicChanged(peer, characteristic, false); + } + Log.d(TAG, "Notified SAKE frame (" + data.length + " bytes)"); + } catch (SecurityException e) { + Log.e(TAG, "Security exception sending SAKE notification", e); + } + } +} diff --git a/app/src/test/java/org/openminimed/pumpconnector/ExampleUnitTest.java b/app/src/test/java/org/openminimed/pumpconnector/ExampleUnitTest.java index e6142ac..145359e 100644 --- a/app/src/test/java/org/openminimed/pumpconnector/ExampleUnitTest.java +++ b/app/src/test/java/org/openminimed/pumpconnector/ExampleUnitTest.java @@ -1,9 +1,9 @@ package org.openminimed.pumpconnector; -import org.junit.Test; - import static org.junit.Assert.*; +import org.junit.Test; + /** * Example local unit test, which will execute on the development machine (host). * diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 273bbb9..757c4c3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,8 @@ appcompat = "1.7.1" material = "1.13.0" activity = "1.12.2" constraintlayout = "2.2.1" +spotless = "6.25.0" +googleJavaFormat = "1.22.0" [libraries] junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -19,4 +21,5 @@ constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayo [plugins] android-application = { id = "com.android.application", version.ref = "agp" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } diff --git a/settings.gradle.kts b/settings.gradle.kts index c77005b..367e74f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,3 +21,5 @@ dependencyResolutionManagement { rootProject.name = "PumpConnector" include(":app") + +includeBuild("JavaSake")