diff --git a/app/build.gradle b/app/build.gradle index 254f226..2e1bdce 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,7 +13,7 @@ android { minSdkVersion 21 targetSdkVersion 31 versionCode 2 - versionName "2.1.0" + versionName "2.1.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/io/heraldprox/herald/app/MainActivity.java b/app/src/main/java/io/heraldprox/herald/app/MainActivity.java index 70ead8d..f32df89 100644 --- a/app/src/main/java/io/heraldprox/herald/app/MainActivity.java +++ b/app/src/main/java/io/heraldprox/herald/app/MainActivity.java @@ -157,56 +157,7 @@ protected void onCreate(Bundle savedInstanceState) { // REQUIRED : Ensure app has all required permissions requestPermissions(); - // Test UI specific process to gather data from sensor for presentation - final Sensor sensor = AppDelegate.getAppDelegate().sensor(); - sensor.add(this); - sensor.add(socialMixingScore); - ((TextView) findViewById(R.id.device)).setText(SensorArray.deviceDescription); - PayloadData payloadData = ((SensorArray) AppDelegate.getAppDelegate().sensor()).payloadData(); - ((TextView) findViewById(R.id.payload)).setText("PAYLOAD : " + payloadData.shortName()); - - if (BLESensorConfiguration.gpdmpEnabled) { - // We generate a static identified and use this for the TEST payload, and for the channel UUID - // DO NOT DO THIS IN PRODUCTION - this is JUST for THIS demo app - mySenderRecipientId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aa" + payloadData.hexEncodedString().substring(10)); - Log.d(tag, "Setting my own GPDMP channel ID (channelID=" + mySenderRecipientId.toString() + ")"); - // Listen for any and all data sent on the default GPDMP channel - ((SensorArray) sensor).getGPDMPMessageListenerManager().addMessageListener(defaultChannelId, this); - } - - targetListAdapter = new TargetListAdapter(this, targets); - final ListView targetsListView = ((ListView) findViewById(R.id.targets)); - targetsListView.setAdapter(targetListAdapter); - targetsListView.setOnItemClickListener(this); - // - Temporal histogram model configuration - ((TemporalHistogramModelView) findViewById(R.id.temporalHistogram)).model(temporalHistogramModel); - ((TemporalHistogramModelView) findViewById(R.id.temporalHistogram)).bin(3); - - // Test programmatic control of sensor on/off - final Switch onOffSwitch = findViewById(R.id.sensorOnOffSwitch); - onOffSwitch.setChecked(false); - onOffSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - sensor.start(); - } else { - sensor.stop(); - } - } - }); - - // Sensor is on by default, unless automated test has been enabled, - // in which case, sensor is off by default and controlled by test - // server remote commands. - final AutomatedTestClient automatedTestClient = AppDelegate.getAppDelegate().automatedTestClient; - if (null == automatedTestClient) { - sensor.stop(); - sensor.start(); // called again (first called by AppDelegate constructor) to ensure permissions are forced - } else { - sensor.stop(); - automatedTestClient.add(this); - } + // All Herald advert/scan start code now moved to onRequestPermissionsResult() } /** @@ -216,6 +167,12 @@ private void requestPermissions() { // Check and request permissions final List requiredPermissions = new ArrayList<>(); + // See https://developer.android.com/guide/topics/connectivity/bluetooth/permissions + // These two are 'legacy' permissions, but in testing are still needed on Android v12+ + requiredPermissions.add(Manifest.permission.BLUETOOTH); + // This is required for power management (I.e. the turn Bluetooth on/off button on the UI) + requiredPermissions.add(Manifest.permission.BLUETOOTH_ADMIN); + // The next two ARE ***BOTH*** STILL NEEDED for Android 12 in order for didMeasure and didRead to work! requiredPermissions.add(Manifest.permission.ACCESS_COARSE_LOCATION); requiredPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION); @@ -233,9 +190,7 @@ private void requestPermissions() { requiredPermissions.add(Manifest.permission.BLUETOOTH_ADVERTISE); } else { // Legacy permissions - // See https://developer.android.com/guide/topics/connectivity/bluetooth/permissions - requiredPermissions.add(Manifest.permission.BLUETOOTH); - requiredPermissions.add(Manifest.permission.BLUETOOTH_ADMIN); + // Note: There are no permissions here as Android documentation for v12+ is incorrect! } final String[] requiredPermissionsArray = requiredPermissions.toArray(new String[0]); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { @@ -269,6 +224,59 @@ public void onRequestPermissionsResult(final int requestCode, @NonNull final Str if (!permissionsGranted) { Log.e(tag, "Application does not have all required permissions to start (permissions=" + Arrays.asList(permissions) + ")"); } + + // Herald sensor initialisation code moved here to prevent mistiming between the advert starting and permissions being granted + + // Test UI specific process to gather data from sensor for presentation + final Sensor sensor = AppDelegate.getAppDelegate().sensor(); + sensor.add(this); + sensor.add(socialMixingScore); + ((TextView) findViewById(R.id.device)).setText(SensorArray.deviceDescription); + PayloadData payloadData = ((SensorArray) AppDelegate.getAppDelegate().sensor()).payloadData(); + ((TextView) findViewById(R.id.payload)).setText("PAYLOAD : " + payloadData.shortName()); + + if (BLESensorConfiguration.gpdmpEnabled) { + // We generate a static identified and use this for the TEST payload, and for the channel UUID + // DO NOT DO THIS IN PRODUCTION - this is JUST for THIS demo app + mySenderRecipientId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aa" + payloadData.hexEncodedString().substring(10)); + Log.d(tag, "Setting my own GPDMP channel ID (channelID=" + mySenderRecipientId.toString() + ")"); + // Listen for any and all data sent on the default GPDMP channel + ((SensorArray) sensor).getGPDMPMessageListenerManager().addMessageListener(defaultChannelId, this); + } + + targetListAdapter = new TargetListAdapter(this, targets); + final ListView targetsListView = ((ListView) findViewById(R.id.targets)); + targetsListView.setAdapter(targetListAdapter); + targetsListView.setOnItemClickListener(this); + // - Temporal histogram model configuration + ((TemporalHistogramModelView) findViewById(R.id.temporalHistogram)).model(temporalHistogramModel); + ((TemporalHistogramModelView) findViewById(R.id.temporalHistogram)).bin(3); + + // Test programmatic control of sensor on/off + final Switch onOffSwitch = findViewById(R.id.sensorOnOffSwitch); + onOffSwitch.setChecked(false); + onOffSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + sensor.start(); + } else { + sensor.stop(); + } + } + }); + + // Sensor is on by default, unless automated test has been enabled, + // in which case, sensor is off by default and controlled by test + // server remote commands. + final AutomatedTestClient automatedTestClient = AppDelegate.getAppDelegate().automatedTestClient; + if (null == automatedTestClient) { + sensor.stop(); + sensor.start(); // called again (first called by AppDelegate constructor) to ensure permissions are forced + } else { + sensor.stop(); + automatedTestClient.add(this); + } } } diff --git a/herald/build.gradle b/herald/build.gradle index 2353ea8..18358e4 100644 --- a/herald/build.gradle +++ b/herald/build.gradle @@ -5,7 +5,7 @@ plugins { } group = 'io.heraldprox' -version = '2.1.0' +version = '2.1.1' apply plugin: 'com.android.library' @@ -17,7 +17,7 @@ android { minSdkVersion 21 targetSdkVersion 31 versionCode 2 - versionName "2.1.0" + versionName "2.1.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" diff --git a/herald/src/main/java/io/heraldprox/herald/sensor/ble/ConcreteBLETransmitter.java b/herald/src/main/java/io/heraldprox/herald/sensor/ble/ConcreteBLETransmitter.java index 1bd22e6..5f37455 100644 --- a/herald/src/main/java/io/heraldprox/herald/sensor/ble/ConcreteBLETransmitter.java +++ b/herald/src/main/java/io/heraldprox/herald/sensor/ble/ConcreteBLETransmitter.java @@ -129,7 +129,8 @@ public void start() { // TODO do we need to do something here in case Sensor.init() is called? E.g. stop and restart? // Override state to starting anyway to ensure we try to restart myLoopTask.doStart(); - myLoopTask.healthCheck(); + // Immediately calling a healthcheck can cause advertising to be stopped before it has had chance to start +// myLoopTask.healthCheck(); } @Override @@ -139,6 +140,8 @@ public void stop() { } else { logger.fault("stop, transmitter already disabled"); } + // Clean out this reference (forced) + bluetoothGattServer = null; } @@ -207,6 +210,8 @@ private void healthCheck() { if (advertLoopState != AdvertLoopState.stopped) { logger.debug("advertLoopTask, healthCheck, stopping advert following bluetooth state change (isSupported={},bluetoothPowerOff={})", isSupported(), bluetoothStateManager.state() == BluetoothState.poweredOff); doStop(now,true); + } else { + logger.debug("advertLoopTask, healthCheck, transmitter is disabled, not supported, or Bluetooth is powered off"); } return; } @@ -339,7 +344,13 @@ private void disableGattServer() { logger.fault("disableGattServer, no BLUETOOTH_CONNECT permission to clear and stop GATT server"); } else { try { - bluetoothGattServer.clearServices(); +// bluetoothGattServer.clearServices(); // Clears non-Herald GATT services (E.g. fetch device name, model, etc.) + BluetoothGattService ourService = bluetoothGattServer.getService(BLESensorConfiguration.linuxFoundationServiceUUID); + if (null != ourService) { + // Service is already advertised, so remove it + logger.debug("disableGattServer clearing single service for Herald (NOT calling clearServices())"); + bluetoothGattServer.removeService(ourService); + } bluetoothGattServer.close(); logger.debug("disableGattServer, GATT server stopped successfully"); } catch (Throwable e) { @@ -519,6 +530,10 @@ public boolean isSupported() { @Override public void bluetoothStateManager(@NonNull final BluetoothState didUpdateState) { logger.debug("didUpdateState (state={},transmitterEnabled={})", didUpdateState, transmitterEnabled.get()); + if (BluetoothState.poweredOff == didUpdateState) { + // Clean out this reference (forced) + bluetoothGattServer = null; + } myLoopTask.healthCheck(); } @@ -620,6 +635,14 @@ private void removeData(@NonNull final BluetoothDevice device) { } } + @Override + public void onServiceAdded(int status, @NonNull BluetoothGattService service) { + logger.debug("BluetoothGattServerCallback, onServiceAdded (successfully={},uuid={}", + (BluetoothGatt.GATT_SUCCESS == status), + service.getUuid().toString() + ); + } + @Override public void onConnectionStateChange(@NonNull final BluetoothDevice bluetoothDevice, final int status, final int newState) { lastInteraction = new Date(); @@ -635,6 +658,7 @@ public void onConnectionStateChange(@NonNull final BluetoothDevice bluetoothDevi myLoopTask.healthCheck(); } + @Override public void onCharacteristicWriteRequest(@NonNull final BluetoothDevice device, final int requestId, @NonNull final BluetoothGattCharacteristic characteristic, final boolean preparedWrite, final boolean responseNeeded, final int offset, @Nullable final byte[] value) { lastInteraction = new Date(); @@ -816,13 +840,20 @@ private static void setGattService(@NonNull final SensorLogger logger, @NonNull logger.debug("setGattService, serviceList, - with characteristic: {}",chr.getUuid()); } } - // TODO decide if we need to not clear other services (I.e. can we interfere with other apps? Worrying if we can...) - bluetoothGattServer.clearServices(); + // Only modify our own service (This should be the only service on GATT we can see anyway) +// bluetoothGattServer.clearServices(); + BluetoothGattService ourService = bluetoothGattServer.getService(BLESensorConfiguration.linuxFoundationServiceUUID); + if (null != ourService) { + // Service is already advertised, so remove it + logger.debug("setGattService clearing single service for Herald (NOT calling clearServices())"); + boolean success = bluetoothGattServer.removeService(ourService); + logger.debug("setGattService clearing single service result (successful={})",success); + } // Logic check - ensure there are now no Gatt Services List services = bluetoothGattServer.getServices(); for (final BluetoothGattService svc : services) { - logger.fault("setGattService device clearServices() call did not correctly clear service (service={})",svc.getUuid()); + logger.debug("setGattService is advertising (non-Herald) service (service={})",svc.getUuid()); } final BluetoothGattService service = new BluetoothGattService(BLESensorConfiguration.linuxFoundationServiceUUID, BluetoothGattService.SERVICE_TYPE_PRIMARY); @@ -858,6 +889,10 @@ private static void setGattService(@NonNull final SensorLogger logger, @NonNull if (count > 1) { logger.fault("setGattService device incorrectly sharing multiple Herald services (count={})", count); } + if (0 == count) { + // Note that the service will probably not be listed yet - updating the advert is asynchronous + logger.fault("setGattService couldn't list Herald services after setting! Should be advertising now (or soon...)."); + } logger.debug("setGattService successful (service={},signalCharacteristic={},payloadCharacteristic={})", service.getUuid(), signalCharacteristic.getUuid(), payloadCharacteristic.getUuid());