diff --git a/README.md b/README.md index e92f8369..df9a2bd1 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Simplicity Connect includes many demos to test sample apps in the Silicon Labs G - **AWS Demo**: This Demo showcases a system where sensor data is sent to AWS IoT Core using the MQTT protocol. A mobile app subscribes to specific MQTT topics to receive this sensor data in real-time. The app can also publish messages to AWS IoT Core, which are then received by the sensor device's firmware, enabling two-way communication. - **Smart Lock**: Add/commission smart lock and read/control it over Bluetooth and AWS IoT Cloud. - **Channel Sounding**: Measure distance between your device and EFRxG24 devices using Bluetooth Channel Sounding with configurable parameters. -- **Energy Harvesting**:Monitor harvested voltage in real time. +- **Energy Harvesting**: Monitor harvested voltage in real time. ## Development Features Simplicity Connect helps developers create and troubleshoot Bluetooth applications running on Silicon Labs’ BLE hardware. Here’s a rundown of some example functionalities. diff --git a/mobile/build.gradle.kts b/mobile/build.gradle.kts index 7bc48de8..897dfb61 100644 --- a/mobile/build.gradle.kts +++ b/mobile/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.api.tasks.Copy + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") @@ -13,6 +15,17 @@ repositories { mavenCentral() } +// Registered Copy task (not assemble.doLast { copy { } }) keeps configuration cache working: +// doLast + copy { } resolves to project.copy() at execution time and captures Project. +tasks.register("copySiConnectReleaseApkToBuilds") { + from(layout.buildDirectory.dir("outputs/apk/Si-Connect/release")) { + include("*.apk") + // Stable name for scripts (e.g. automation_scripts/launch_apk.sh) + rename { _ -> "mobile-Si-Connect-release.apk" } + } + into(layout.projectDirectory.dir("Builds")) +} + android { compileSdk = 36 namespace = "com.siliconlabs.bledemo" @@ -60,15 +73,6 @@ android { - applicationVariants.all{ - assembleProvider.get().doLast{ - copy{ - from("Builds/${rootProject.name}/${project.name}/outputs/apk/Si-Connect/release/mobile-Si-Connect-release.apk") - into ("Builds") - } - } - } - lint { checkReleaseBuilds = false // Or, if you prefer, you can continue to check for errors in release builds, @@ -83,8 +87,8 @@ android { create("Si-Connect") { dimension = versionDim applicationId = "com.siliconlabs.bledemo" - versionCode = 76 - versionName = "3.2.2" + versionCode = 82 + versionName = "3.3.0" } } @@ -118,9 +122,15 @@ android { val apkName = "mobile-${name}-${versionName}.apk" (this as? com.android.build.gradle.internal.api.BaseVariantOutputImpl)?.outputFileName = apkName } + if (buildType.name == "release") { + assembleProvider.configure { + finalizedBy(tasks.named("copySiConnectReleaseApkToBuilds")) + } + } } } + java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 diff --git a/mobile/ic_launcher_source/ic_si_connect_figma.png b/mobile/ic_launcher_source/ic_si_connect_figma.png new file mode 100644 index 00000000..5fdd67c9 Binary files /dev/null and b/mobile/ic_launcher_source/ic_si_connect_figma.png differ diff --git a/mobile/ic_launcher_source/ic_si_connect_figma_no_border.png b/mobile/ic_launcher_source/ic_si_connect_figma_no_border.png new file mode 100644 index 00000000..70be8c3a Binary files /dev/null and b/mobile/ic_launcher_source/ic_si_connect_figma_no_border.png differ diff --git a/mobile/libs/AndroidPlatform.jar b/mobile/libs/AndroidPlatform.jar index 4a9c722d..1bcf49a4 100644 Binary files a/mobile/libs/AndroidPlatform.jar and b/mobile/libs/AndroidPlatform.jar differ diff --git a/mobile/libs/CHIPClusterID.jar b/mobile/libs/CHIPClusterID.jar index 962006ca..07e97bc3 100644 Binary files a/mobile/libs/CHIPClusterID.jar and b/mobile/libs/CHIPClusterID.jar differ diff --git a/mobile/libs/CHIPClusters.jar b/mobile/libs/CHIPClusters.jar index 0266d475..d39f3b36 100644 Binary files a/mobile/libs/CHIPClusters.jar and b/mobile/libs/CHIPClusters.jar differ diff --git a/mobile/libs/CHIPController.jar b/mobile/libs/CHIPController.jar index 9edf4182..cebc6160 100644 Binary files a/mobile/libs/CHIPController.jar and b/mobile/libs/CHIPController.jar differ diff --git a/mobile/libs/CHIPInteractionModel.jar b/mobile/libs/CHIPInteractionModel.jar index b11534f9..da5b4487 100644 Binary files a/mobile/libs/CHIPInteractionModel.jar and b/mobile/libs/CHIPInteractionModel.jar differ diff --git a/mobile/libs/OnboardingPayload.jar b/mobile/libs/OnboardingPayload.jar index 58ed1d58..6f3f5e24 100644 Binary files a/mobile/libs/OnboardingPayload.jar and b/mobile/libs/OnboardingPayload.jar differ diff --git a/mobile/libs/gdx-1.9.6-SNAPSHOT.jar b/mobile/libs/gdx-1.9.6-SNAPSHOT.jar deleted file mode 100644 index 93af6c33..00000000 Binary files a/mobile/libs/gdx-1.9.6-SNAPSHOT.jar and /dev/null differ diff --git a/mobile/libs/gdx-backend-android-1.9.6-SNAPSHOT.jar b/mobile/libs/gdx-backend-android-1.9.6-SNAPSHOT.jar deleted file mode 100644 index 543dd306..00000000 Binary files a/mobile/libs/gdx-backend-android-1.9.6-SNAPSHOT.jar and /dev/null differ diff --git a/mobile/libs/libMatterJson.jar b/mobile/libs/libMatterJson.jar index d43dfe0b..3031c8b3 100644 Binary files a/mobile/libs/libMatterJson.jar and b/mobile/libs/libMatterJson.jar differ diff --git a/mobile/libs/libMatterTlv.jar b/mobile/libs/libMatterTlv.jar index d8e3f4dd..df423223 100644 Binary files a/mobile/libs/libMatterTlv.jar and b/mobile/libs/libMatterTlv.jar differ diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 408c8a30..39d39da0 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -45,7 +45,8 @@ android:name=".application.SiliconLabsDemoApplication" android:allowBackup="false" android:enableOnBackInvokedCallback="true" - android:icon="@mipmap/si_launcher_round" + android:icon="@mipmap/ic_launcher_rebranded" + android:roundIcon="@mipmap/ic_launcher_rebranded" android:label="@string/app_name_simplicity_Connect" android:requestLegacyExternalStorage="true" android:supportsRtl="true" @@ -53,9 +54,7 @@ android:usesCleartextTraffic="true" tools:replace="android:allowBackup" tools:targetApi="tiramisu"> - + + + + + + + + + + tools:ignore="DiscouragedApi" + tools:targetApi="36" /> + \ No newline at end of file diff --git a/mobile/src/main/efr_redesign_launcher-playstore.png b/mobile/src/main/efr_redesign_launcher-playstore.png deleted file mode 100644 index 01ad7fcf..00000000 Binary files a/mobile/src/main/efr_redesign_launcher-playstore.png and /dev/null differ diff --git a/mobile/src/main/ic_launcher_source/ic_si_connect_figma.png b/mobile/src/main/ic_launcher_source/ic_si_connect_figma.png new file mode 100644 index 00000000..5fdd67c9 Binary files /dev/null and b/mobile/src/main/ic_launcher_source/ic_si_connect_figma.png differ diff --git a/mobile/src/main/ic_launcher_source/ic_si_connect_figma_no_border.png b/mobile/src/main/ic_launcher_source/ic_si_connect_figma_no_border.png new file mode 100644 index 00000000..70be8c3a Binary files /dev/null and b/mobile/src/main/ic_launcher_source/ic_si_connect_figma_no_border.png differ diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/Bluetooth/BLE/BluetoothDeviceInfo.kt b/mobile/src/main/java/com/siliconlabs/bledemo/Bluetooth/BLE/BluetoothDeviceInfo.kt index 8ae5f12f..7d899a6b 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/Bluetooth/BLE/BluetoothDeviceInfo.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/Bluetooth/BLE/BluetoothDeviceInfo.kt @@ -6,10 +6,10 @@ import android.util.Log import androidx.core.util.isEmpty import com.siliconlabs.bledemo.bluetooth.beacon_utils.BleFormat import com.siliconlabs.bledemo.bluetooth.beacon_utils.BleFormat.Companion.getFormat -import java.util.* import kotlin.math.min -class BluetoothDeviceInfo(var device: BluetoothDevice, var isFavorite: Boolean = false) : Cloneable { +class BluetoothDeviceInfo(var device: BluetoothDevice, var isFavorite: Boolean = false) : + Cloneable { var connectionState = ConnectionState.DISCONNECTED var isConnectable = false @@ -23,7 +23,6 @@ class BluetoothDeviceInfo(var device: BluetoothDevice, var isFavorite: Boolean = var timestampLast: Long = 0 - public override fun clone(): BluetoothDeviceInfo { val retVal: BluetoothDeviceInfo try { @@ -72,7 +71,8 @@ class BluetoothDeviceInfo(var device: BluetoothDevice, var isFavorite: Boolean = this.intervalNanos = intervalNanos else if (intervalNanos < this.intervalNanos + 3000000) { val limitedCount = min(count, 10) - this.intervalNanos = (this.intervalNanos * (limitedCount - 1) + intervalNanos) / limitedCount + this.intervalNanos = + (this.intervalNanos * (limitedCount - 1) + intervalNanos) / limitedCount } else if (intervalNanos < this.intervalNanos * 1.4) { this.intervalNanos = (this.intervalNanos * 29 + intervalNanos) / 30 } @@ -106,6 +106,7 @@ class BluetoothDeviceInfo(var device: BluetoothDevice, var isFavorite: Boolean = val manufacturer: DeviceManufacturer get() = scanInfo?.scanRecord?.manufacturerSpecificData?.let { + println("---------------MAnufacture:$it") if (it.isEmpty()) DeviceManufacturer.UNKNOWN else when (it.keyAt(0)) { MANUFACTURER_VALUE_WINDOWS -> DeviceManufacturer.WINDOWS @@ -115,6 +116,7 @@ class BluetoothDeviceInfo(var device: BluetoothDevice, var isFavorite: Boolean = enum class DeviceManufacturer { WINDOWS, + ANDROID, UNKNOWN } diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/Bluetooth/BLE/ScanResultCompat.kt b/mobile/src/main/java/com/siliconlabs/bledemo/Bluetooth/BLE/ScanResultCompat.kt index 606d4e46..323bf5df 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/Bluetooth/BLE/ScanResultCompat.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/Bluetooth/BLE/ScanResultCompat.kt @@ -30,7 +30,10 @@ class ScanResultCompat { @SuppressLint("MissingPermission") fun getDisplayName(): String { - return device?.name ?: "N/A" + // Priority: scan record device name > BluetoothDevice.name > fallback + return scanRecord?.deviceName + ?: device?.name + ?: "N/A" } override fun toString(): String { diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/Bluetooth/Services/BluetoothService.kt b/mobile/src/main/java/com/siliconlabs/bledemo/Bluetooth/Services/BluetoothService.kt index 05ecfc20..c8bdfb48 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/Bluetooth/Services/BluetoothService.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/Bluetooth/Services/BluetoothService.kt @@ -1,10 +1,11 @@ package com.siliconlabs.bledemo.bluetooth.services import android.annotation.SuppressLint -import android.app.Notification -import android.app.NotificationChannel +// GATT-server debug notification (disabled) — uncomment if re-enabling: +// import android.app.Notification +// import android.app.NotificationChannel import android.app.NotificationManager -import android.app.PendingIntent +// import android.app.PendingIntent import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt @@ -46,7 +47,7 @@ import com.siliconlabs.bledemo.bluetooth.ble.ConnectedDeviceInfo import com.siliconlabs.bledemo.bluetooth.ble.GattConnection import com.siliconlabs.bledemo.bluetooth.ble.ScanResultCompat import com.siliconlabs.bledemo.bluetooth.ble.TimeoutGattCallback -import com.siliconlabs.bledemo.features.configure.advertiser.activities.PendingServerConnectionActivity +// import com.siliconlabs.bledemo.features.configure.advertiser.activities.PendingServerConnectionActivity import com.siliconlabs.bledemo.features.configure.advertiser.services.AdvertiserService import com.siliconlabs.bledemo.features.configure.gatt_configurator.utils.BluetoothGattServicesCreator import com.siliconlabs.bledemo.features.configure.gatt_configurator.utils.GattConfiguratorStorage @@ -56,7 +57,7 @@ import com.siliconlabs.bledemo.features.scan.browser.models.logs.GattOperationWi import com.siliconlabs.bledemo.features.scan.browser.models.logs.GattOperationWithParameterLog import com.siliconlabs.bledemo.features.scan.browser.models.logs.Log import com.siliconlabs.bledemo.features.scan.browser.models.logs.TimeoutLog -import com.siliconlabs.bledemo.home_screen.activities.MainActivity +// import com.siliconlabs.bledemo.home_screen.activities.MainActivity import com.siliconlabs.bledemo.home_screen.activities.MainActivity.Companion.ACTION_SHOW_CUSTOM_TOAST import com.siliconlabs.bledemo.home_screen.activities.MainActivity.Companion.EXTRA_TOAST_MESSAGE import com.siliconlabs.bledemo.home_screen.menu_items.HealthThermometer @@ -88,17 +89,18 @@ class BluetoothService : LocalService() { private const val RSSI_UPDATE_FREQUENCY = 2000L private const val REFRESH_SERVICES_DELAY = 500L // give device time refresh cache - private const val ACTION_GATT_SERVER_DEBUG_CONNECTION = - "com.siliconlabs.bledemo.action.GATT_SERVER_DEBUG_CONNECTION" - private const val ACTION_GATT_SERVER_REMOVE_NOTIFICATION = - "com.siliconlabs.bledemo.action.GATT_SERVER_REMOVE_NOTIFICATION" const val ACTION_SHOW_BOND_LOSS_DIALOG = "com.siliconlabs.bledemo.action.SHOW_BOND_LOSS_DIALOG" - private const val GATT_SERVER_REMOVE_NOTIFICATION_REQUEST_CODE = 666 - private const val GATT_SERVER_DEBUG_CONNECTION_REQUEST_CODE = 888 - private const val GATT_SERVER_OPEN_CONNECTION_REQUEST_CODE = 777 + // GATT-server debug notification (disabled) — companion symbols for restore: + // private const val ACTION_GATT_SERVER_DEBUG_CONNECTION = + // "com.siliconlabs.bledemo.action.GATT_SERVER_DEBUG_CONNECTION" + // private const val ACTION_GATT_SERVER_REMOVE_NOTIFICATION = + // "com.siliconlabs.bledemo.action.GATT_SERVER_REMOVE_NOTIFICATION" + // private const val GATT_SERVER_REMOVE_NOTIFICATION_REQUEST_CODE = 666 + // private const val GATT_SERVER_DEBUG_CONNECTION_REQUEST_CODE = 888 + // private const val GATT_SERVER_OPEN_CONNECTION_REQUEST_CODE = 777 private const val NOTIFICATION_ID = 999 - private const val CHANNEL_ID = "DEBUG_CONNECTION_CHANNEL" + // private const val CHANNEL_ID = "DEBUG_CONNECTION_CHANNEL" const val EXTRA_BLUETOOTH_DEVICE = "EXTRA_BLUETOOTH_DEVICE" const val EXTRA_DEVICE_ADDRESS = "EXTRA_DEVICE_ADDRESS" } @@ -252,7 +254,7 @@ class BluetoothService : LocalService() { registerReceiver(scanReceiver, IntentFilter(BluetoothDevice.ACTION_FOUND)) registerReceiver(bluetoothReceiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)) registerReceiver(locationReceiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)) - registerGattServerReceiver() + // GATT-server debug notification (disabled): registerGattServerReceiver() initGattServer() // Register for KEY_MISSING and ENCRYPTION_CHANGE intents val filter = IntentFilter().apply { @@ -350,19 +352,18 @@ class BluetoothService : LocalService() { } } - private fun registerGattServerReceiver() { - val filter = IntentFilter().apply { - addAction(ACTION_GATT_SERVER_DEBUG_CONNECTION) - addAction(ACTION_GATT_SERVER_REMOVE_NOTIFICATION) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - registerReceiver(gattServerBroadcastReceiver, filter, RECEIVER_EXPORTED) - } else { - registerReceiver(gattServerBroadcastReceiver, filter) - } - - } + // --- GATT-server debug connection notification (disabled; uncomment imports/constants, onCreate/onDestroy, and the large block before closeGattServerNotification) --- + // private fun registerGattServerReceiver() { + // val filter = IntentFilter().apply { + // addAction(ACTION_GATT_SERVER_DEBUG_CONNECTION) + // addAction(ACTION_GATT_SERVER_REMOVE_NOTIFICATION) + // } + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // registerReceiver(gattServerBroadcastReceiver, filter, RECEIVER_EXPORTED) + // } else { + // registerReceiver(gattServerBroadcastReceiver, filter) + // } + // } override fun onDestroy() { stopDiscovery() @@ -372,10 +373,6 @@ class BluetoothService : LocalService() { unregisterReceiver(scanReceiver) } catch (_: IllegalArgumentException) { } - try { - unregisterReceiver(gattServerBroadcastReceiver) - } catch (_: IllegalArgumentException) { - } try { unregisterReceiver(bluetoothReceiver) } catch (_: IllegalArgumentException) { @@ -384,6 +381,8 @@ class BluetoothService : LocalService() { unregisterReceiver(locationReceiver) } catch (_: IllegalArgumentException) { } + // GATT-server debug notification (disabled): + // try { unregisterReceiver(gattServerBroadcastReceiver) } catch (_: IllegalArgumentException) {} try { unregisterReceiver(keyMissingAndEncryptionChangeReceiver) } catch (_: IllegalArgumentException) { @@ -1093,9 +1092,11 @@ class BluetoothService : LocalService() { when (newState) { BluetoothGatt.STATE_CONNECTED -> if (status == BluetoothGatt.GATT_SUCCESS) { - if (gattServerCallback != null) { - gattServerCallback?.onConnectionStateChange(device, status, newState) - } else if (isNotificationEnabled) showDebugConnectionNotification(device) + // GATT-server debug notification disabled; previous behavior: + // if (gattServerCallback != null) { + // gattServerCallback?.onConnectionStateChange(device, status, newState) + // } else if (isNotificationEnabled) showDebugConnectionNotification(device) + gattServerCallback?.onConnectionStateChange(device, status, newState) } } } @@ -1242,116 +1243,102 @@ class BluetoothService : LocalService() { return devicesToIndicate[characteristicUuid] ?: emptySet() } - private fun getYesPendingIntent(device: BluetoothDevice): PendingIntent { - val intent = Intent(ACTION_GATT_SERVER_DEBUG_CONNECTION) - intent.putExtra(EXTRA_BLUETOOTH_DEVICE, device) - - val pendingIntentFlag = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE - else PendingIntent.FLAG_CANCEL_CURRENT - - return PendingIntent.getBroadcast( - this, - GATT_SERVER_DEBUG_CONNECTION_REQUEST_CODE, - intent, - pendingIntentFlag - ) - } - - private fun getNoPendingIntent(): PendingIntent { - val intent = Intent(ACTION_GATT_SERVER_REMOVE_NOTIFICATION) - val pendingIntentFlag = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE - else PendingIntent.FLAG_CANCEL_CURRENT - - return PendingIntent.getBroadcast( - this, - GATT_SERVER_REMOVE_NOTIFICATION_REQUEST_CODE, - intent, - pendingIntentFlag - ) - } - - private fun getYesAndOpenPendingIntent(device: BluetoothDevice): PendingIntent { - val intent = Intent(this, PendingServerConnectionActivity::class.java).apply { - putExtra(EXTRA_BLUETOOTH_DEVICE, device) - flags = Intent.FLAG_ACTIVITY_NO_HISTORY - } - val backIntent = Intent(this, MainActivity::class.java).apply { - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - } - - val pendingIntentFlag = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE - else PendingIntent.FLAG_ONE_SHOT - - return PendingIntent.getActivities( - this, - GATT_SERVER_OPEN_CONNECTION_REQUEST_CODE, - arrayOf(backIntent, intent), - pendingIntentFlag - ) - } - - private fun showDebugConnectionNotification(device: BluetoothDevice) { - val deviceName = device.name ?: getString(R.string.not_advertising_shortcut) - createNotificationChannel() - - val notification = Notification.Builder(this, CHANNEL_ID) - .setSmallIcon(R.mipmap.si_launcher) - .setContentTitle( - getString( - R.string.notification_title_device_has_connected, - deviceName - ) - ) - .setContentText(getString(R.string.notification_note_debug_connection)) - .addAction(buildAction(getString(R.string.button_yes), getYesPendingIntent(device))) - /* .addAction(buildAction(getString(R.string.notification_button_yes_and_open), getYesAndOpenPendingIntent(device)))*/ - .addAction(buildAction(getString(R.string.button_no), getNoPendingIntent())) - .build() - - val notificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.notify(NOTIFICATION_ID, notification) - } - - private fun createNotificationChannel() { - val name = getString(R.string.notification_channel_name) - val descriptionText = getString(R.string.notification_channel_description) - val importance = NotificationManager.IMPORTANCE_HIGH - val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { - description = descriptionText - } - - val notificationManager: NotificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(channel) - } - - private fun buildAction(actionText: String, actionIntent: PendingIntent): Notification.Action { - return Notification.Action.Builder( - R.mipmap.si_launcher, - actionText, - actionIntent - ).build() - } - - private val gattServerBroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - when (intent?.action) { - ACTION_GATT_SERVER_DEBUG_CONNECTION -> { - val device = intent.getParcelableExtra(EXTRA_BLUETOOTH_DEVICE) - device?.let { - connectGatt(device, false, null) - } - closeGattServerNotification() - } - - ACTION_GATT_SERVER_REMOVE_NOTIFICATION -> closeGattServerNotification() - } - } - } + // private fun getYesPendingIntent(device: BluetoothDevice): PendingIntent { + // val intent = Intent(ACTION_GATT_SERVER_DEBUG_CONNECTION) + // intent.putExtra(EXTRA_BLUETOOTH_DEVICE, device) + // val pendingIntentFlag = + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE + // else PendingIntent.FLAG_CANCEL_CURRENT + // return PendingIntent.getBroadcast( + // this, + // GATT_SERVER_DEBUG_CONNECTION_REQUEST_CODE, + // intent, + // pendingIntentFlag + // ) + // } + // + // private fun getNoPendingIntent(): PendingIntent { + // val intent = Intent(ACTION_GATT_SERVER_REMOVE_NOTIFICATION) + // val pendingIntentFlag = + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE + // else PendingIntent.FLAG_CANCEL_CURRENT + // return PendingIntent.getBroadcast( + // this, + // GATT_SERVER_REMOVE_NOTIFICATION_REQUEST_CODE, + // intent, + // pendingIntentFlag + // ) + // } + // + // private fun getYesAndOpenPendingIntent(device: BluetoothDevice): PendingIntent { + // val intent = Intent(this, PendingServerConnectionActivity::class.java).apply { + // putExtra(EXTRA_BLUETOOTH_DEVICE, device) + // flags = Intent.FLAG_ACTIVITY_NO_HISTORY + // } + // val backIntent = Intent(this, MainActivity::class.java).apply { + // addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + // } + // val pendingIntentFlag = + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE + // else PendingIntent.FLAG_ONE_SHOT + // return PendingIntent.getActivities( + // this, + // GATT_SERVER_OPEN_CONNECTION_REQUEST_CODE, + // arrayOf(backIntent, intent), + // pendingIntentFlag + // ) + // } + // + // private fun showDebugConnectionNotification(device: BluetoothDevice) { + // val deviceName = device.name ?: getString(R.string.not_advertising_shortcut) + // createNotificationChannel() + // val notification = Notification.Builder(this, CHANNEL_ID) + // .setSmallIcon(R.mipmap.si_launcher) + // .setContentTitle( + // getString(R.string.notification_title_device_has_connected, deviceName) + // ) + // .setContentText(getString(R.string.notification_note_debug_connection)) + // .addAction(buildAction(getString(R.string.button_yes), getYesPendingIntent(device))) + // // .addAction(buildAction(getString(R.string.notification_button_yes_and_open), getYesAndOpenPendingIntent(device))) + // .addAction(buildAction(getString(R.string.button_no), getNoPendingIntent())) + // .build() + // val notificationManager = + // getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + // notificationManager.notify(NOTIFICATION_ID, notification) + // } + // + // private fun createNotificationChannel() { + // val name = getString(R.string.notification_channel_name) + // val descriptionText = getString(R.string.notification_channel_description) + // val importance = NotificationManager.IMPORTANCE_HIGH + // val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { + // description = descriptionText + // } + // val notificationManager: NotificationManager = + // getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + // notificationManager.createNotificationChannel(channel) + // } + // + // private fun buildAction(actionText: String, actionIntent: PendingIntent): Notification.Action { + // return Notification.Action.Builder( + // R.mipmap.si_launcher, + // actionText, + // actionIntent + // ).build() + // } + // + // private val gattServerBroadcastReceiver = object : BroadcastReceiver() { + // override fun onReceive(context: Context?, intent: Intent?) { + // when (intent?.action) { + // ACTION_GATT_SERVER_DEBUG_CONNECTION -> { + // val device = intent.getParcelableExtra(EXTRA_BLUETOOTH_DEVICE) + // device?.let { connectGatt(it, false, null) } + // closeGattServerNotification() + // } + // ACTION_GATT_SERVER_REMOVE_NOTIFICATION -> closeGattServerNotification() + // } + // } + // } fun closeGattServerNotification() { val notificationManager = diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/Utils/AppUtil.kt b/mobile/src/main/java/com/siliconlabs/bledemo/Utils/AppUtil.kt index c5e7a4ce..9c3dcd52 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/Utils/AppUtil.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/Utils/AppUtil.kt @@ -22,8 +22,10 @@ object AppUtil { window.statusBarColor = Color.TRANSPARENT window.navigationBarColor = Color.TRANSPARENT WindowInsetsControllerCompat(window, window.decorView).let { controller -> - controller.isAppearanceLightStatusBars = true // adjust for dark/light icons - controller.isAppearanceLightNavigationBars = true + // Status bar background is dark (#333333), so icons must be rendered in white. + // isAppearanceLightStatusBars = false => white (light) icons on dark background. + controller.isAppearanceLightStatusBars = false + controller.isAppearanceLightNavigationBars = false } activity.findViewById(R.id.fakeStatusBar)?.let { fakeStatusBar -> diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/Utils/ApppUtil.kt b/mobile/src/main/java/com/siliconlabs/bledemo/Utils/ApppUtil.kt deleted file mode 100644 index cf15bd0c..00000000 --- a/mobile/src/main/java/com/siliconlabs/bledemo/Utils/ApppUtil.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.siliconlabs.bledemo.utils - - -import android.app.Activity -import android.content.Context -import android.graphics.Color -import android.view.View -import android.view.Window -import androidx.core.content.ContextCompat -import androidx.core.view.ViewCompat -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsControllerCompat -import androidx.core.view.updatePadding -import com.siliconlabs.bledemo.R -import com.siliconlabs.bledemo.home_screen.views.HidableBottomNavigationView - -object ApppUtil { - - fun setEdgeToEdge(window: Window, activity: Activity) { - WindowCompat.setDecorFitsSystemWindows(window, false) - window.statusBarColor = Color.TRANSPARENT - window.navigationBarColor = Color.TRANSPARENT - WindowInsetsControllerCompat(window, window.decorView).let { controller -> - controller.isAppearanceLightStatusBars = true // adjust for dark/light icons - controller.isAppearanceLightNavigationBars = true - } - - activity.findViewById(R.id.fakeStatusBar)?.let { fakeStatusBar -> - ViewCompat.setOnApplyWindowInsetsListener(fakeStatusBar) { v, insets -> - val statusBarHeight = - insets.getInsets(WindowInsetsCompat.Type.statusBars()).top - v.layoutParams = v.layoutParams.apply { height = statusBarHeight } - v.requestLayout() - insets - } - } - - activity.findViewById(R.id.main_navigation) - ?.let { bottomNav -> - ViewCompat.setOnApplyWindowInsetsListener(bottomNav) { v, insets -> - val navBarHeight = - insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom - v.updatePadding(bottom = navBarHeight) - insets - } - } - } - - fun optOutEdgeToEdge(window: Window,context: Context) { - WindowCompat.setDecorFitsSystemWindows(window, true) - window.statusBarColor = ContextCompat.getColor(context,R.color.blue_primary) - window.navigationBarColor = ContextCompat.getColor(context,R.color.blue_primary) - } -} - diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/Utils/CustomToastManager.kt b/mobile/src/main/java/com/siliconlabs/bledemo/Utils/CustomToastManager.kt index 2fd83515..28a07e16 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/Utils/CustomToastManager.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/Utils/CustomToastManager.kt @@ -3,22 +3,43 @@ package com.siliconlabs.bledemo.utils import android.annotation.SuppressLint import android.content.Context +import android.view.Gravity +import android.widget.TextView +import android.widget.Toast +import androidx.core.content.res.ResourcesCompat import com.pranavpandey.android.dynamic.toasts.DynamicToast +import com.pranavpandey.android.dynamic.toasts.R as DynamicToastLibR +import com.siliconlabs.bledemo.R object CustomToastManager { + + private fun applyToastTypeface(context: Context) { + ResourcesCompat.getFont(context.applicationContext, R.font.stolzl_medium)?.let { tf -> + DynamicToast.Config.getInstance().setTextTypeface(tf).apply() + } + } + + private fun centerToastMessage(toast: Toast) { + toast.view?.findViewById(DynamicToastLibR.id.adt_toast_text)?.gravity = + Gravity.CENTER_HORIZONTAL + } + @SuppressLint("StaticFieldLeak") fun showError(context: Context, message: String, duration: Long = 5000) { - DynamicToast.makeError(context, message, duration.toInt()).show() + applyToastTypeface(context) + DynamicToast.makeError(context, message, duration.toInt()).also { centerToastMessage(it) }.show() } @SuppressLint("StaticFieldLeak") fun show(context: Context, message: String, duration: Long = 5000) { - DynamicToast.make(context, message, duration.toInt()).show() + applyToastTypeface(context) + DynamicToast.make(context, message, duration.toInt()).also { centerToastMessage(it) }.show() } fun showSuccess(context: Context, message: String, duration: Long = 5000) { - DynamicToast.makeSuccess(context, message, duration.toInt()).show() + applyToastTypeface(context) + DynamicToast.makeSuccess(context, message, duration.toInt()).also { centerToastMessage(it) }.show() } } diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/Utils/DisplayUtils.kt b/mobile/src/main/java/com/siliconlabs/bledemo/Utils/DisplayUtils.kt new file mode 100644 index 00000000..5242190f --- /dev/null +++ b/mobile/src/main/java/com/siliconlabs/bledemo/Utils/DisplayUtils.kt @@ -0,0 +1,17 @@ +package com.siliconlabs.bledemo.utils + +import android.app.Activity +import android.graphics.Point +import android.os.Build + +object DisplayUtils { + + fun getScreenWidth(activity: Activity): Int { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + activity.windowManager.currentWindowMetrics.bounds.width() + } else { + @Suppress("DEPRECATION") + Point().also { activity.windowManager.defaultDisplay.getSize(it) }.x + } + } +} diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/Utils/UuidConsts.kt b/mobile/src/main/java/com/siliconlabs/bledemo/Utils/UuidConsts.kt index adb83f83..8935124d 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/Utils/UuidConsts.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/Utils/UuidConsts.kt @@ -1,6 +1,7 @@ package com.siliconlabs.bledemo.utils -import java.util.* +import java.util.Locale +import java.util.UUID object UuidConsts { val OTA_SERVICE: UUID = UUID.fromString("1d14d6ee-fd63-4fa1-bfa4-8f47b42119f0") @@ -11,4 +12,24 @@ object UuidConsts { val DEVICE_NAME: UUID = UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb") val CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") + + /** CS Initiator lock-state service (16-bit 0xAABB, Bluetooth SIG base UUID). */ + val DIGITAL_KEY_SERVICE: UUID = UUID.fromString("0000aabb-0000-1000-8000-00805f9b34fb") + + /** Lock state characteristic: read + notify, 1 byte (0x00 unlock, 0x01 lock). */ + val DIGITAL_KEY_LOCK_STATE: UUID = UUID.fromString("0000bbcc-0000-1000-8000-00805f9b34fb") + + const val DIGITAL_KEY_SERVICE_UUID16: Int = 0xAABB + const val DIGITAL_KEY_LOCK_CHARACTERISTIC_UUID16: Int = 0xBBCC +} + +/** SIG Bluetooth base UUID for 16-bit assigned numbers (e.g. AABB, BBCC, 2902). */ +fun bluetoothUuidFrom16Bits(shortUuid16: Int): UUID { + return UUID.fromString( + String.format(Locale.US, "0000%04x-0000-1000-8000-00805f9b34fb", shortUuid16 and 0xFFFF) + ) +} + +fun isBluetoothUuid16(uuid: UUID, shortUuid16: Int): Boolean { + return uuid == bluetoothUuidFrom16Bits(shortUuid16) } diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/advertiser/activities/AdvertiserConfigActivity.kt b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/advertiser/activities/AdvertiserConfigActivity.kt index 564a4938..9abcfc89 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/advertiser/activities/AdvertiserConfigActivity.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/advertiser/activities/AdvertiserConfigActivity.kt @@ -378,19 +378,19 @@ class AdvertiserConfigActivity : BaseActivity(), IAdvertiserConfigActivityView { ) { val legacyAdapter = ArrayAdapter( this, - R.layout.spinner_item_layout_medium, + R.layout.spinner_item_advertiser_config_type, translator.getValuesAsStringList(legacyModes) ) - legacyAdapter.setDropDownViewResource(R.layout.spinner_dropdown_item_layout) + legacyAdapter.setDropDownViewResource(R.layout.spinner_dropdown_advertiser_config_type) binding.advConfigType.spLegacy.adapter = legacyAdapter val extendedAdapter = ArrayAdapter( this, - R.layout.spinner_item_layout_medium, + R.layout.spinner_item_advertiser_config_type, translator.getValuesAsStringList(extendedModes) ) - extendedAdapter.setDropDownViewResource(R.layout.spinner_dropdown_item_layout) + extendedAdapter.setDropDownViewResource(R.layout.spinner_dropdown_advertiser_config_type) binding.advConfigType.spExtended.adapter = extendedAdapter if (isLegacy) binding.advConfigType.tvExtendedAdvNotSupported.visibility = View.VISIBLE diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/advertiser/activities/PendingServerConnectionActivity.kt b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/advertiser/activities/PendingServerConnectionActivity.kt index 3aded196..5967683f 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/advertiser/activities/PendingServerConnectionActivity.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/advertiser/activities/PendingServerConnectionActivity.kt @@ -30,8 +30,9 @@ class PendingServerConnectionActivity : BaseActivity() { window.statusBarColor = Color.TRANSPARENT window.navigationBarColor = Color.TRANSPARENT WindowInsetsControllerCompat(window,window.decorView).apply { - isAppearanceLightStatusBars = true // adjust for dark/light icons - isAppearanceLightNavigationBars = true + // Status bar background is dark (#333333), so icons must be rendered in white. + isAppearanceLightStatusBars = false + isAppearanceLightNavigationBars = false } setContentView(R.layout.activity_pending_server_connection) diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/advertiser/services/AdvertiserService.kt b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/advertiser/services/AdvertiserService.kt index 6d6a01d9..40c91e3d 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/advertiser/services/AdvertiserService.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/advertiser/services/AdvertiserService.kt @@ -75,7 +75,7 @@ class AdvertiserService : Service() { return Notification.Builder(this, CHANNEL_ID).apply { setContentTitle("Si Connect") setContentText("Advertiser is running...") - setSmallIcon(R.mipmap.si_launcher) + setSmallIcon(R.mipmap.ic_launcher_rebranded) setContentIntent(pendingIntent) setShowWhen(false) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/dialogs/CharacteristicDialog.kt b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/dialogs/CharacteristicDialog.kt index 88b86003..dc3cc055 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/dialogs/CharacteristicDialog.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/dialogs/CharacteristicDialog.kt @@ -1,15 +1,25 @@ package com.siliconlabs.bledemo.features.configure.gatt_configurator.dialogs +import android.app.Dialog +import android.graphics.Rect import android.os.Bundle import android.text.Editable import android.text.TextWatcher +import android.view.Gravity import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.view.WindowManager import android.widget.AdapterView +import androidx.core.view.WindowCompat import android.widget.ArrayAdapter import android.widget.AutoCompleteTextView import android.widget.CheckBox +import android.widget.EditText +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import com.siliconlabs.bledemo.R import com.siliconlabs.bledemo.base.fragments.BaseDialogFragment import com.siliconlabs.bledemo.databinding.DialogGattServerCharacteristicBinding @@ -18,15 +28,68 @@ import com.siliconlabs.bledemo.features.configure.gatt_configurator.models.* import com.siliconlabs.bledemo.features.configure.gatt_configurator.utils.GattUtils import com.siliconlabs.bledemo.features.configure.gatt_configurator.utils.Validator -//import kotlinx.android.synthetic.main.dialog_gatt_server_characteristic.* -//import kotlinx.android.synthetic.main.gatt_configurator_initial_value.* -//import kotlinx.android.synthetic.main.dialog_add_characteristic_properties_content.* class CharacteristicDialog( val listener: CharacteristicChangeListener, val characteristic: Characteristic = Characteristic() ) : BaseDialogFragment() { private lateinit var binding: DialogGattServerCharacteristicBinding + private var globalFocusChangeListener: ViewTreeObserver.OnGlobalFocusChangeListener? = null + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + window?.let { window -> + WindowCompat.setDecorFitsSystemWindows(window, false) + // Let IME insets drive translation; avoid fighting adjustResize on edge-to-edge hosts. + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) + } + } + } + + override fun onStart() { + super.onStart() + val displayMetrics = resources.displayMetrics + dialog?.window?.apply { + setGravity(Gravity.TOP or Gravity.CENTER_HORIZONTAL) + val params = attributes + params.verticalMargin = DIALOG_TOP_MARGIN_FRACTION + attributes = params + setLayout( + (displayMetrics.widthPixels * DIALOG_WIDTH_SCREEN_FRACTION).toInt(), + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + capScrollableContentHeight() + registerGlobalFocusListener() + } + + private fun registerGlobalFocusListener() { + if (globalFocusChangeListener != null) return + globalFocusChangeListener = + ViewTreeObserver.OnGlobalFocusChangeListener { _, newFocus -> + if (!shouldSlideDialogForFocus(newFocus)) { + binding.root.translationY = 0f + } + ViewCompat.requestApplyInsets(binding.root) + } + dialog?.window?.decorView?.viewTreeObserver?.addOnGlobalFocusChangeListener( + globalFocusChangeListener!! + ) + } + + private fun capScrollableContentHeight() { + val maxScrollHeight = + (resources.displayMetrics.heightPixels * DIALOG_SCROLL_MAX_HEIGHT_FRACTION).toInt() + binding.svCharacteristicContent.post { + val content = binding.svCharacteristicContent.getChildAt(0) ?: return@post + val contentHeight = content.measuredHeight + if (contentHeight > maxScrollHeight) { + binding.svCharacteristicContent.layoutParams.height = maxScrollHeight + binding.svCharacteristicContent.requestLayout() + } + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -48,7 +111,92 @@ class CharacteristicDialog( handleNameChanges() handlePropertyStateChanges() handleInitialValueEditTextChanges() + setupKeyboardInsets() + setupInitialValueKeyboardScrolling() prepopulateFields() + updateSaveButtonState() + } + + override fun onDestroyView() { + globalFocusChangeListener?.let { listener -> + dialog?.window?.decorView?.viewTreeObserver + ?.removeOnGlobalFocusChangeListener(listener) + } + globalFocusChangeListener = null + super.onDestroyView() + } + + /** + * Only slide the dialog for bottom initial-value fields. Name/UUID stay fixed at the top. + */ + private fun shouldSlideDialogForFocus(focused: View?): Boolean { + var view = focused + while (view != null) { + when (view.id) { + R.id.et_initial_value_text, R.id.et_initial_value_hex -> return true + R.id.actv_characteristic_name, R.id.actv_characteristic_uuid -> return false + } + view = view.parent as? View + } + return false + } + + private fun currentFocusedView(): View? = + dialog?.currentFocus ?: binding.root.findFocus() + + private fun applyKeyboardTranslation(imeBottom: Int) { + val slide = imeBottom > 0 && shouldSlideDialogForFocus(currentFocusedView()) + binding.root.translationY = if (slide) -imeBottom.toFloat() else 0f + } + + private fun setupKeyboardInsets() { + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets -> + val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) + applyKeyboardTranslation(imeInsets.bottom) + insets + } + + preventSlideForTopInputField(binding.actvCharacteristicName) + preventSlideForTopInputField(binding.actvCharacteristicUuid) + + ViewCompat.requestApplyInsets(binding.root) + } + + private fun preventSlideForTopInputField(field: View) { + field.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + binding.root.translationY = 0f + } + false + } + field.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) binding.root.translationY = 0f + } + } + + private fun setupInitialValueKeyboardScrolling() { + val scrollToField = { view: View -> + binding.svCharacteristicContent.post { + val scrollView = binding.svCharacteristicContent + val targetRect = Rect() + view.getDrawingRect(targetRect) + scrollView.offsetDescendantRectToMyCoords(view, targetRect) + scrollView.requestRectangleOnScreen(targetRect, true) + } + } + + binding.initialValue.etInitialValueText.setOnFocusChangeListener { view, hasFocus -> + if (hasFocus) { + ViewCompat.requestApplyInsets(binding.root) + scrollToField(view) + } + } + binding.initialValue.etInitialValueHex.setOnFocusChangeListener { view, hasFocus -> + if (hasFocus) { + ViewCompat.requestApplyInsets(binding.root) + scrollToField(view) + } + } } private fun handleClickEvents() { @@ -71,10 +219,10 @@ class CharacteristicDialog( prepopulateProperties() prepopulatePropertyTypes() prepopulateInitialValue() + applyInitialValueFieldVisibility() } private fun prepopulateProperties() { - // binding.propertiesContent.apply { characteristic.properties.apply { binding.propertiesContent.swRead.isChecked = containsKey(Property.READ) binding.propertiesContent.swWrite.isChecked = containsKey(Property.WRITE) @@ -85,7 +233,6 @@ class CharacteristicDialog( binding.propertiesContent.swNotify.isChecked = containsKey(Property.NOTIFY) binding.propertiesContent.swIndicate.isChecked = containsKey(Property.INDICATE) } - } private fun prepopulatePropertyTypes() { @@ -152,10 +299,12 @@ class CharacteristicDialog( getSelectedReadTypes() if (binding.propertiesContent.swWrite.isChecked) characteristic.properties[Property.WRITE] = getSelectedWriteTypes() - if (binding.propertiesContent.swWriteWithoutResp.isChecked) characteristic.properties[Property.WRITE_WITHOUT_RESPONSE] = - getSelectedWriteTypes() - if (binding.propertiesContent.swReliableWrite.isChecked) characteristic.properties[Property.RELIABLE_WRITE] = - getSelectedWriteTypes() + if (binding.propertiesContent.swWriteWithoutResp.isChecked) { + characteristic.properties[Property.WRITE_WITHOUT_RESPONSE] = getSelectedWriteTypes() + } + if (binding.propertiesContent.swReliableWrite.isChecked) { + characteristic.properties[Property.RELIABLE_WRITE] = getSelectedWriteTypes() + } if (binding.propertiesContent.swNotify.isChecked) characteristic.properties[Property.NOTIFY] = hashSetOf() if (binding.propertiesContent.swIndicate.isChecked) characteristic.properties[Property.INDICATE] = @@ -167,13 +316,18 @@ class CharacteristicDialog( if (binding.propertiesContent.swReliableWrite.isChecked) setReliableWritePropertyDescriptor() else removeReliableWritePropertyDescriptor() - if (binding.propertiesContent.swIndicate.isChecked || binding.propertiesContent.swNotify.isChecked) setIndicateOrNotifyPropertyDescriptor() - else removeIndicateOrNotifyPropertyDescriptor() + if (binding.propertiesContent.swIndicate.isChecked || binding.propertiesContent.swNotify.isChecked) { + setIndicateOrNotifyPropertyDescriptor() + } else { + removeIndicateOrNotifyPropertyDescriptor() + } } private fun setReliableWritePropertyDescriptor() { val result = - characteristic.descriptors.filter { it.name == GattUtils.getReliableWriteDescriptor().name && it.isPredefined } + characteristic.descriptors.filter { + it.name == GattUtils.getReliableWriteDescriptor().name && it.isPredefined + } if (result.isEmpty()) { characteristic.descriptors.add(GattUtils.getReliableWriteDescriptor()) } @@ -181,13 +335,17 @@ class CharacteristicDialog( private fun removeReliableWritePropertyDescriptor() { val descriptor = - characteristic.descriptors.find { it.name == GattUtils.getReliableWriteDescriptor().name && it.isPredefined } + characteristic.descriptors.find { + it.name == GattUtils.getReliableWriteDescriptor().name && it.isPredefined + } characteristic.descriptors.remove(descriptor) } private fun setIndicateOrNotifyPropertyDescriptor() { val result = - characteristic.descriptors.filter { it.name == GattUtils.getIndicateOrNotifyDescriptor().name && it.isPredefined } + characteristic.descriptors.filter { + it.name == GattUtils.getIndicateOrNotifyDescriptor().name && it.isPredefined + } if (result.isEmpty()) { characteristic.descriptors.add(GattUtils.getIndicateOrNotifyDescriptor()) } @@ -195,7 +353,9 @@ class CharacteristicDialog( private fun removeIndicateOrNotifyPropertyDescriptor() { val descriptor = - characteristic.descriptors.find { it.name == GattUtils.getIndicateOrNotifyDescriptor().name && it.isPredefined } + characteristic.descriptors.find { + it.name == GattUtils.getIndicateOrNotifyDescriptor().name && it.isPredefined + } characteristic.descriptors.remove(descriptor) } @@ -245,7 +405,7 @@ class CharacteristicDialog( override fun afterTextChanged(s: Editable?) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - binding.btnSave.isEnabled = isInputValid() + updateSaveButtonState() } }) } @@ -256,44 +416,80 @@ class CharacteristicDialog( override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { val len = s?.length - if ((len == 8 || len == 13 || len == 18 || len == 23) && count > before) binding.actvCharacteristicUuid.append( - "-" - ) - binding.btnSave.isEnabled = isInputValid() + if ((len == 8 || len == 13 || len == 18 || len == 23) && count > before) { + binding.actvCharacteristicUuid.append("-") + } + updateSaveButtonState() } }) } private fun handleInitialValueEditTextChanges() { - binding.initialValue.etInitialValueText.addTextChangedListener(object : TextWatcher { + val textWatcher = object : TextWatcher { override fun afterTextChanged(s: Editable?) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - binding.btnSave.isEnabled = isInputValid() + updateSaveButtonState() } - }) + } + binding.initialValue.etInitialValueText.addTextChangedListener(textWatcher) + binding.initialValue.etInitialValueHex.addTextChangedListener(textWatcher) + } - binding.initialValue.etInitialValueHex.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable?) {} - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - binding.btnSave.isEnabled = isInputValid() - } - }) + private fun updateSaveButtonState() { + binding.btnSave.isEnabled = canEnableSave() } - private fun isAnyPropertyChecked(): Boolean { - return binding.propertiesContent.swRead.isChecked || - binding.propertiesContent.swWrite.isChecked || - binding.propertiesContent.swWriteWithoutResp.isChecked || - binding.propertiesContent.swReliableWrite.isChecked || - binding.propertiesContent.swNotify.isChecked || - binding.propertiesContent.swIndicate.isChecked + private fun canEnableSave(): Boolean { + if (!isCharacteristicNameFilled()) { + return false + } + if (hasEmptyRequiredCharacteristicField()) { + return false + } + if (!isInitialValueRequirementMet()) { + return false + } + return isUuidValidWhenFilled() + } + + private fun isCharacteristicNameFilled(): Boolean { + return binding.actvCharacteristicName.text.toString().trim().isNotEmpty() + } + + /** Name and UUID only; initial value fields follow [isInitialValueRequirementMet] by spinner. */ + private fun hasEmptyRequiredCharacteristicField(): Boolean { + return isVisibleEnabledEmptyTextField(binding.actvCharacteristicName) || + isVisibleEnabledEmptyTextField(binding.actvCharacteristicUuid) + } + + private fun isVisibleEnabledEmptyTextField(field: EditText): Boolean { + return field.visibility == View.VISIBLE && + field.isEnabled && + field.text.toString().trim().isEmpty() + } + + private fun isInitialValueRequirementMet(): Boolean { + return when (binding.initialValue.spInitialValue.selectedItemPosition) { + POSITION_INITIAL_VALUE_EMPTY -> true + POSITION_INITIAL_VALUE_TEXT -> + binding.initialValue.etInitialValueText.text.toString().trim().isNotEmpty() + POSITION_INITIAL_VALUE_HEX -> + binding.initialValue.etInitialValueHex.text.toString().trim().isNotEmpty() + else -> true + } + } + + private fun isUuidValidWhenFilled(): Boolean { + val uuidText = binding.actvCharacteristicUuid.text.toString().trim() + if (uuidText.isEmpty()) { + return false + } + return isUuidValid(uuidText) } private fun handlePropertyStateChanges() { binding.propertiesContent.swRead.setOnCheckedChangeListener { _, _ -> - binding.btnSave.isEnabled = isInputValid() setPropertyParametersState( binding.propertiesContent.swRead.isChecked, binding.propertiesContent.cbReadBonded, @@ -301,35 +497,32 @@ class CharacteristicDialog( ) } binding.propertiesContent.swWrite.setOnCheckedChangeListener { _, _ -> - binding.btnSave.isEnabled = isInputValid() setPropertyParametersState( - binding.propertiesContent.swWrite.isChecked || binding.propertiesContent.swWriteWithoutResp.isChecked || binding.propertiesContent.swReliableWrite.isChecked, + binding.propertiesContent.swWrite.isChecked || + binding.propertiesContent.swWriteWithoutResp.isChecked || + binding.propertiesContent.swReliableWrite.isChecked, binding.propertiesContent.cbWriteBonded, binding.propertiesContent.cbWriteMitm ) } binding.propertiesContent.swWriteWithoutResp.setOnCheckedChangeListener { _, _ -> - binding.btnSave.isEnabled = isInputValid() setPropertyParametersState( - binding.propertiesContent.swWrite.isChecked || binding.propertiesContent.swWriteWithoutResp.isChecked || binding.propertiesContent.swReliableWrite.isChecked, + binding.propertiesContent.swWrite.isChecked || + binding.propertiesContent.swWriteWithoutResp.isChecked || + binding.propertiesContent.swReliableWrite.isChecked, binding.propertiesContent.cbWriteBonded, binding.propertiesContent.cbWriteMitm ) } binding.propertiesContent.swReliableWrite.setOnCheckedChangeListener { _, _ -> - binding.btnSave.isEnabled = isInputValid() setPropertyParametersState( - binding.propertiesContent.swWrite.isChecked || binding.propertiesContent.swWriteWithoutResp.isChecked || binding.propertiesContent.swReliableWrite.isChecked, + binding.propertiesContent.swWrite.isChecked || + binding.propertiesContent.swWriteWithoutResp.isChecked || + binding.propertiesContent.swReliableWrite.isChecked, binding.propertiesContent.cbWriteBonded, binding.propertiesContent.cbWriteMitm ) } - binding.propertiesContent.swNotify.setOnCheckedChangeListener { _, _ -> - binding.btnSave.isEnabled = isInputValid() - } - binding.propertiesContent.swIndicate.setOnCheckedChangeListener { _, _ -> - binding.btnSave.isEnabled = isInputValid() - } } private fun setPropertyParametersState( @@ -345,30 +538,6 @@ class CharacteristicDialog( } } - private fun isInputValid(): Boolean { - return isAnyPropertyChecked() - && binding.actvCharacteristicName.text.toString().isNotEmpty() - && isUuidValid(binding.actvCharacteristicUuid.text.toString()) - && isInitialValueValid() - } - - private fun isInitialValueValid(): Boolean { - when (binding.initialValue.spInitialValue.selectedItemPosition) { - POSITION_INITIAL_VALUE_EMPTY -> { - return true - } - - POSITION_INITIAL_VALUE_TEXT -> { - return binding.initialValue.etInitialValueText.text.toString().isNotEmpty() - } - - POSITION_INITIAL_VALUE_HEX -> { - return Validator.isHexValid(binding.initialValue.etInitialValueHex.text.toString()) - } - } - return true - } - private fun isUuidValid(uuid: String): Boolean { return Validator.is16BitUuidValid(uuid) || Validator.is128BitUuidValid(uuid) } @@ -382,10 +551,11 @@ class CharacteristicDialog( actv.setAdapter(adapter) actv.setOnItemClickListener { _, _, position, _ -> - val characteristic = adapter.getItem(position) - binding.actvCharacteristicName.setText(characteristic?.name) - binding.actvCharacteristicUuid.setText(characteristic?.getIdentifierAsString()) + val selected = adapter.getItem(position) + binding.actvCharacteristicName.setText(selected?.name) + binding.actvCharacteristicUuid.setText(selected?.getIdentifierAsString()) actv.setSelection(actv.length()) + updateSaveButtonState() hideKeyboard() } } @@ -411,15 +581,26 @@ class CharacteristicDialog( position: Int, id: Long ) { - binding.btnSave.isEnabled = isInputValid() - binding.initialValue.etInitialValueText.visibility = - if (position == POSITION_INITIAL_VALUE_TEXT) View.VISIBLE else View.GONE - binding.initialValue.llInitialValueHex.visibility = - if (position == POSITION_INITIAL_VALUE_HEX) View.VISIBLE else View.GONE + applyInitialValueFieldVisibility() + updateSaveButtonState() } - override fun onNothingSelected(parent: AdapterView<*>?) { - } + override fun onNothingSelected(parent: AdapterView<*>?) {} + } + } + + private fun applyInitialValueFieldVisibility() { + when (binding.initialValue.spInitialValue.selectedItemPosition) { + POSITION_INITIAL_VALUE_TEXT -> + binding.initialValue.etInitialValueText.visibility = View.VISIBLE + else -> + binding.initialValue.etInitialValueText.visibility = View.GONE + } + binding.initialValue.llInitialValueHex.visibility = + if (binding.initialValue.spInitialValue.selectedItemPosition == POSITION_INITIAL_VALUE_HEX) { + View.VISIBLE + } else { + View.GONE } } @@ -439,6 +620,8 @@ class CharacteristicDialog( binding.initialValue.spInitialValue.setSelection(0) binding.initialValue.etInitialValueText.setText("") binding.initialValue.etInitialValueHex.setText("") + applyInitialValueFieldVisibility() + updateSaveButtonState() } interface CharacteristicChangeListener { @@ -449,5 +632,9 @@ class CharacteristicDialog( private const val POSITION_INITIAL_VALUE_EMPTY = 0 private const val POSITION_INITIAL_VALUE_TEXT = 1 private const val POSITION_INITIAL_VALUE_HEX = 2 + + private const val DIALOG_WIDTH_SCREEN_FRACTION = 0.9f + private const val DIALOG_SCROLL_MAX_HEIGHT_FRACTION = 0.55f + private const val DIALOG_TOP_MARGIN_FRACTION = 0.05f } } \ No newline at end of file diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/dialogs/ServiceDialog.kt b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/dialogs/ServiceDialog.kt index 5394bdc6..c3beff3b 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/dialogs/ServiceDialog.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/dialogs/ServiceDialog.kt @@ -48,6 +48,7 @@ class ServiceDialog(val listener: ServiceChangeListener, var service: Service = handleClickEvents() handleNameChanges() handleUuidChanges() + updateSaveButtonState() } private fun initACTV(actv: AutoCompleteTextView, searchMode: SearchMode) { @@ -60,6 +61,7 @@ class ServiceDialog(val listener: ServiceChangeListener, var service: Service = binding.actvServiceName.setText(service?.name) binding.actvServiceUuid.setText(service?.getIdentifierAsString()) actv.setSelection(actv.length()) + updateSaveButtonState() hideKeyboard() } } @@ -174,7 +176,7 @@ class ServiceDialog(val listener: ServiceChangeListener, var service: Service = override fun afterTextChanged(s: Editable?) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - binding.btnSave.isEnabled = isInputValid() + updateSaveButtonState() setMandatoryRequirementsCheckBoxState() setServiceTypeSpinnerState() } @@ -191,13 +193,20 @@ class ServiceDialog(val listener: ServiceChangeListener, var service: Service = "-" ) - binding.btnSave.isEnabled = isInputValid() setMandatoryRequirementsCheckBoxState() setServiceTypeSpinnerState() } }) } + private fun updateSaveButtonState() { + binding.btnSave.isEnabled = isServiceNameFilled() + } + + private fun isServiceNameFilled(): Boolean { + return binding.actvServiceName.text.toString().trim().isNotEmpty() + } + private fun setMandatoryRequirementsCheckBoxState() { if (isInput16BitService()) { binding.cbMandatoryRequirements.isEnabled = true @@ -225,11 +234,6 @@ class ServiceDialog(val listener: ServiceChangeListener, var service: Service = return result.isNotEmpty() } - private fun isInputValid(): Boolean { - return binding.actvServiceName.text.toString() - .isNotEmpty() && isUuidValid(binding.actvServiceUuid.text.toString()) - } - private fun isUuidValid(uuid: String): Boolean { return Validator.is16BitUuidValid(uuid) || Validator.is128BitUuidValid(uuid) } @@ -246,6 +250,7 @@ class ServiceDialog(val listener: ServiceChangeListener, var service: Service = binding.actvServiceUuid.setText("") binding.cbMandatoryRequirements.isChecked = false binding.spServiceType.setSelection(0) + updateSaveButtonState() } interface ServiceChangeListener { diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/viewholders/AddServiceViewHolder.kt b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/viewholders/AddServiceViewHolder.kt index 815a7c6f..fa07488a 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/viewholders/AddServiceViewHolder.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/viewholders/AddServiceViewHolder.kt @@ -30,7 +30,11 @@ class AddServiceViewHolder( parent: ViewGroup, addServiceListener: AddServiceListener ): AddServiceViewHolder { - val binding = AdapterAddServiceBinding.inflate(LayoutInflater.from(parent.context)) + val binding = AdapterAddServiceBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) return AddServiceViewHolder(binding, addServiceListener) } diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/viewholders/GattServerViewHolder.kt b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/viewholders/GattServerViewHolder.kt index 1e9652e7..9f9f7632 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/viewholders/GattServerViewHolder.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/viewholders/GattServerViewHolder.kt @@ -30,13 +30,13 @@ class GattServerViewHolder( swGattServer.isChecked = gattServer.isSwitchedOn } - prepareView(isExportMode) + prepareView(isExportMode, gattServer) prepareDetailsView(gattServer) handleClickActions(gattServer) handleSwitchActions() } - private fun prepareView(isExportMode: Boolean) { + private fun prepareView(isExportMode: Boolean, gattServer: GattServer) { viewBinding.apply { cbExport.visibility = if (isExportMode) View.VISIBLE else View.GONE swGattServer.isEnabled = !isExportMode @@ -45,7 +45,7 @@ class GattServerViewHolder( toggleImageButton(ibEdit, !isExportMode) toggleImageButton(ibRemove, !isExportMode) - if (!isExportMode) cbExport.isChecked = false + cbExport.isChecked = isExportMode && gattServer.isCheckedForExport } } diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/viewmodels/GattConfiguratorViewModel.kt b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/viewmodels/GattConfiguratorViewModel.kt index 0f75073d..b1a81409 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/viewmodels/GattConfiguratorViewModel.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/features/configure/gatt_configurator/viewmodels/GattConfiguratorViewModel.kt @@ -35,7 +35,7 @@ class GattConfiguratorViewModel @Inject constructor(val gattConfiguratorStorage: _gattServers.value?.apply { gattServer?.let { add(gattServer) - } ?: add(GattServer("New GATT server")) + } ?: add(GattServer("New GATT Server")) _insertedPosition.value = size - 1 } areAnyGattServers() diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/features/demo/awsiot/AWSIOTDemoActivity.kt b/mobile/src/main/java/com/siliconlabs/bledemo/features/demo/awsiot/AWSIOTDemoActivity.kt index f20e425d..a9fa22f6 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/features/demo/awsiot/AWSIOTDemoActivity.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/features/demo/awsiot/AWSIOTDemoActivity.kt @@ -23,6 +23,7 @@ import android.widget.TextView import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager @@ -40,9 +41,9 @@ import com.siliconlabs.bledemo.features.demo.awsiot.model.GridItem import com.siliconlabs.bledemo.features.demo.awsiot.repository.ConnectionResult import com.siliconlabs.bledemo.features.demo.awsiot.viewmodel.MqttViewModel import com.siliconlabs.bledemo.features.demo.matter_demo.utils.CustomProgressDialog -import com.siliconlabs.bledemo.utils.ApppUtil import com.siliconlabs.bledemo.features.demo.smartlock.dialogs.SmartLockConfigurationDialog import com.siliconlabs.bledemo.features.demo.smartlock.dialogs.SmartLockConfigurationDialog.Companion.PICK_P12_FILE_REQUEST_CODE +import com.siliconlabs.bledemo.utils.AppUtil import com.siliconlabs.bledemo.utils.CustomToastManager import org.json.JSONException import org.json.JSONObject @@ -79,6 +80,26 @@ class AWSIOTDemoActivity : AppCompatActivity(), OnMqttGridItemClickListener { private var isBlueOn = false private var awsConfigDialog: SmartLockConfigurationDialog? = null + /** True while waiting for the first successful [updateGrid] after connect; drives red toolbar ProgressBar. */ + private var pendingInitialAwsGrid = false + + private val mqttMessagesObserver = Observer { payload -> + if (payload.isBlank()) return@Observer + + timeoutRunnable?.let { runnable -> handler.removeCallbacks(runnable) } + + if (!isValidJsonObject(payload)) { + return@Observer + } + + removeProgress() + Log.e("AWS DEMO ACTIVITY", "AWS IOT MESSAGES$payload") + + runOnUiThread { + updateGrid(JSONObject(payload)) + } + } + companion object { private const val PREFS_NAME = "MqttPrefs" @@ -99,13 +120,15 @@ class AWSIOTDemoActivity : AppCompatActivity(), OnMqttGridItemClickListener { super.onCreate(savedInstanceState) binding = ActivityAwsDemoBinding.inflate(layoutInflater) setContentView(binding.root) - ApppUtil.setEdgeToEdge(window, this) + AppUtil.setEdgeToEdge(window, this) setSupportActionBar(binding.toolbar) val actionBar = supportActionBar actionBar!!.setHomeAsUpIndicator(R.drawable.matter_back) actionBar.setDisplayHomeAsUpEnabled(true) actionBar?.title = getString(R.string.aws_dashboard) - actionBar?.setBackgroundDrawable(ColorDrawable(Color.parseColor("#0F62FE"))) + actionBar?.setBackgroundDrawable( + ColorDrawable(ContextCompat.getColor(this, R.color.aws_iot_demo_toolbar)) + ) sharedPreferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) initUI() @@ -131,17 +154,28 @@ class AWSIOTDemoActivity : AppCompatActivity(), OnMqttGridItemClickListener { onBackPressedDispatcher.addCallback(this, backPressedCallback) } + private fun startAwsInitialLoadProgress() { + pendingInitialAwsGrid = true + binding.awsIotLoadProgress.visibility = View.VISIBLE + } + + private fun cancelAwsInitialLoadProgress() { + pendingInitialAwsGrid = false + binding.awsIotLoadProgress.visibility = View.GONE + } + private fun initObservers() { mqttViewModel.connectionResult.observe(this, Observer { result -> when (result) { is ConnectionResult.Connecting -> { + startAwsInitialLoadProgress() showProgressDialog(getString(R.string.connecting_to_aws)) } is ConnectionResult.Connected -> { - + startAwsInitialLoadProgress() removeProgress() runOnUiThread { CustomToastManager.show( @@ -155,6 +189,8 @@ class AWSIOTDemoActivity : AppCompatActivity(), OnMqttGridItemClickListener { } is ConnectionResult.Error -> { + cancelAwsInitialLoadProgress() + mqttViewModel.mqttMessages.removeObserver(mqttMessagesObserver) // Handle the error println("Connection Error: ${result.message}") result.throwable?.printStackTrace() // Print the stack trace for debugging @@ -183,27 +219,11 @@ class AWSIOTDemoActivity : AppCompatActivity(), OnMqttGridItemClickListener { ConnectionResult.SubscribeConnected -> { binding.placeholder.visibility = View.GONE showProgressDialog(getString(R.string.loading_data)) - // Start timeout timer startMessageTimeout() - mqttViewModel.mqttMessages.observe(this, Observer { - // Cancel timeout if message received - handler.removeCallbacks(timeoutRunnable!!) - removeProgress() - Log.e("AWS DEMO ACTIVITY", "AWS IOT MESSAGES" + it.toString()) - - val jsonString = it.toString() // Replace with real JSON source - if (isValidJsonObject(jsonString)){ - runOnUiThread { - updateGrid(JSONObject(jsonString)) - } - - }else{ - // do not do anything - } - - - }) + mqttViewModel.prepareMqttObservationForNewSubscription() + mqttViewModel.mqttMessages.removeObserver(mqttMessagesObserver) + mqttViewModel.mqttMessages.observe(this, mqttMessagesObserver) } ConnectionResult.SubscribeConnecting -> { @@ -211,6 +231,8 @@ class AWSIOTDemoActivity : AppCompatActivity(), OnMqttGridItemClickListener { } ConnectionResult.Disconnected -> { + cancelAwsInitialLoadProgress() + mqttViewModel.mqttMessages.removeObserver(mqttMessagesObserver) removeProgress() //this.finish() } @@ -220,6 +242,8 @@ class AWSIOTDemoActivity : AppCompatActivity(), OnMqttGridItemClickListener { } ConnectionResult.DisconnectionError -> { + cancelAwsInitialLoadProgress() + mqttViewModel.mqttMessages.removeObserver(mqttMessagesObserver) this.finish() } } @@ -228,7 +252,9 @@ class AWSIOTDemoActivity : AppCompatActivity(), OnMqttGridItemClickListener { // Function to start the timeout private fun startMessageTimeout() { + timeoutRunnable?.let { handler.removeCallbacks(it) } timeoutRunnable = Runnable { + cancelAwsInitialLoadProgress() removeProgress() showAlertDialogForRetry() } @@ -290,7 +316,7 @@ class AWSIOTDemoActivity : AppCompatActivity(), OnMqttGridItemClickListener { motionItem = GridItem( "Motion", motionData, - R.drawable.icon_dks_917_motion + R.drawable.ic_motion_container ) } // Process other keys according to ordered key @@ -354,7 +380,7 @@ class AWSIOTDemoActivity : AppCompatActivity(), OnMqttGridItemClickListener { if (motionItem != null) { // Ensure there are at least 5 items by adding placeholders if needed while (newItems.size < 4) { - newItems.add(GridItem("", "", R.drawable.icon_dks_917_motion))//placeholder icon + newItems.add(GridItem("", "", R.drawable.ic_motion_container))//placeholder icon } newItems.add(motionItem) } @@ -364,6 +390,14 @@ class AWSIOTDemoActivity : AppCompatActivity(), OnMqttGridItemClickListener { // Update the adapter adapter.updateData(newItems) + if (pendingInitialAwsGrid) { + binding.mqttRv.post { + if (pendingInitialAwsGrid) { + pendingInitialAwsGrid = false + binding.awsIotLoadProgress.visibility = View.GONE + } + } + } } private fun getIconForKey(key: String): Int { @@ -372,7 +406,7 @@ class AWSIOTDemoActivity : AppCompatActivity(), OnMqttGridItemClickListener { getString(R.string.aws_humidity) -> R.drawable.icon_environment getString(R.string.aws_ambient_light) -> R.drawable.icon_light getString(R.string.aws_white_light) -> R.drawable.icon_light - getString(R.string.motion_demo_title) -> R.drawable.icon_dks_917_motion + getString(R.string.motion_demo_title) -> R.drawable.ic_motion_container else -> R.drawable.background_grey_box } @@ -551,13 +585,14 @@ class AWSIOTDemoActivity : AppCompatActivity(), OnMqttGridItemClickListener { println("SSLContext ${sslContext.toString()}") if(isNetworkAvailable(this)) { runOnUiThread { + startAwsInitialLoadProgress() DynamicToast.makeSuccess( this, getString(R.string.smart_lock_device_connected), 5000 ) + mqttViewModel.connect(subscribeTopic, endPoint, sslContext) } - mqttViewModel.connect(subscribeTopic, endPoint, sslContext) sslContext }else{ runOnUiThread { @@ -791,6 +826,9 @@ class AWSIOTDemoActivity : AppCompatActivity(), OnMqttGridItemClickListener { } override fun onDestroy() { + timeoutRunnable?.let { handler.removeCallbacks(it) } + mqttViewModel.mqttMessages.removeObserver(mqttMessagesObserver) + cancelAwsInitialLoadProgress() backPressedCallback.remove() stopMqttForegroundService() cancelNotification() diff --git a/mobile/src/main/java/com/siliconlabs/bledemo/features/demo/awsiot/adapter/GridAdapter.kt b/mobile/src/main/java/com/siliconlabs/bledemo/features/demo/awsiot/adapter/GridAdapter.kt index 72b6505d..7f052460 100644 --- a/mobile/src/main/java/com/siliconlabs/bledemo/features/demo/awsiot/adapter/GridAdapter.kt +++ b/mobile/src/main/java/com/siliconlabs/bledemo/features/demo/awsiot/adapter/GridAdapter.kt @@ -16,6 +16,7 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.button.MaterialButton import com.siliconlabs.bledemo.R import com.siliconlabs.bledemo.features.demo.awsiot.AWSIOTDemoActivity import com.siliconlabs.bledemo.features.demo.awsiot.listener.OnMqttGridItemClickListener @@ -149,13 +150,13 @@ class GridAdapter(private var items: List, val yesBtn: TextView = devKitSensorDialog.findViewById(R.id.yes_opt) yesBtn.visibility = View.GONE val noBtn: TextView = devKitSensorDialog.findViewById(R.id.no_opt) - val onLEDBtn = devKitSensorDialog.findViewById