From 506016d0251db76b4fb402c0793ffe63b721cea8 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 27 Sep 2026 17:16:17 +0200 Subject: [PATCH 1/4] Fix Wear OS crash when Bluetooth is disabled --- .../wearos/BluetoothServerService.kt | 86 +++++++++++++------ .../wearos/WearSyncServiceManager.kt | 25 +----- app/src/main/res/values/strings.xml | 2 + 3 files changed, 64 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt b/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt index 17670195b3..dee5589183 100644 --- a/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt +++ b/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt @@ -1,5 +1,6 @@ package protect.card_locker.wearos +import android.Manifest import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service @@ -7,11 +8,15 @@ import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothServerSocket import android.bluetooth.BluetoothSocket +import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.os.Build import android.os.IBinder import android.util.Log +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import org.json.JSONArray import org.json.JSONObject import protect.card_locker.DBHelper @@ -33,21 +38,76 @@ class BluetoothServerService : Service() { private const val TAG = "CatimaBtServer" private const val NOTIFICATION_ID = NotificationInfo.WearBluetooth.NOTIFICATION_ID private const val CHANNEL_ID = NotificationInfo.WearBluetooth.CHANNEL_ID + + fun showBluetoothServerError(context: Context, error: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + NotificationInfo.WearBluetooth.CRITICAL_ERROR_CHANNEL_ID, + context.getString(R.string.wear_bt_critical_error_channel_name), + NotificationManager.IMPORTANCE_HIGH + ).apply { setShowBadge(false) } + context.getSystemService(NotificationManager::class.java).createNotificationChannel(channel) + } + val notification = NotificationCompat.Builder(context, NotificationInfo.WearBluetooth.CRITICAL_ERROR_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification_error) + .setContentTitle(context.getString(R.string.wear_bt_failed_foreground_notification_title)) + .setContentText(error) + .build() + with(NotificationManagerCompat.from(context)) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + return@with + } + notify( + NotificationInfo.WearBluetooth.CRITICAL_ERROR_NOTIFICATION_ID, + notification + ) + } + } } private var serverThread: AcceptThread? = null override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + // First, create the foreground service with notification + // Android is very picky about requiring the foreground service to be started quickly and will crash the entire app if that doesn't happen + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + getString(R.string.wear_bt_channel_name), + NotificationManager.IMPORTANCE_MIN + ).apply { setShowBadge(false) } + getSystemService(NotificationManager::class.java).createNotificationChannel(channel) + } + val notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.wear_bt_notification_title)) + .setSmallIcon(R.drawable.ic_notification_sync) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setSilent(true) + .build() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startForeground(NOTIFICATION_ID, notification, + android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE) + } else { + startForeground(NOTIFICATION_ID, notification) + } + + // After we have a running notification, try error handling and stopping the service if we have issues if (!BluetoothPermissionHelper.isBluetoothConnectGranted(this)) { Log.w(TAG, "BLUETOOTH_CONNECT permission not granted, stopping") + showBluetoothServerError(this, getString(R.string.wear_sync_permission_required)) stopSelf() return START_NOT_STICKY } val adapter = (getSystemService(BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter if (adapter == null || !adapter.isEnabled) { Log.w(TAG, "Bluetooth not available or disabled") + showBluetoothServerError(this, getString(R.string.bluetooth_disabled)) stopSelf() - return START_NOT_STICKY + return START_STICKY } // Avoid tearing down a working accept socket every time Settings is resumed. @@ -56,36 +116,12 @@ class BluetoothServerService : Service() { return START_STICKY } - startForegroundWithNotification() serverThread?.cancel() serverThread = AcceptThread(adapter).also { it.start() } Log.d(TAG, "Bluetooth server started") return START_STICKY } - private fun startForegroundWithNotification() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - CHANNEL_ID, - getString(R.string.wear_bt_channel_name), - NotificationManager.IMPORTANCE_MIN - ).apply { setShowBadge(false) } - getSystemService(NotificationManager::class.java).createNotificationChannel(channel) - } - val notification = NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle(getString(R.string.wear_bt_notification_title)) - .setSmallIcon(R.drawable.ic_notification_sync) - .setPriority(NotificationCompat.PRIORITY_MIN) - .setSilent(true) - .build() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - startForeground(NOTIFICATION_ID, notification, - android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE) - } else { - startForeground(NOTIFICATION_ID, notification) - } - } - override fun onDestroy() { serverThread?.cancel() serverThread = null diff --git a/app/src/main/java/protect/card_locker/wearos/WearSyncServiceManager.kt b/app/src/main/java/protect/card_locker/wearos/WearSyncServiceManager.kt index 8498dc0ee7..0d73b34f5a 100644 --- a/app/src/main/java/protect/card_locker/wearos/WearSyncServiceManager.kt +++ b/app/src/main/java/protect/card_locker/wearos/WearSyncServiceManager.kt @@ -128,30 +128,7 @@ object WearSyncServiceManager { // We naively assume we have enough permissions to show a notification as the service should never try to start // before the user gave notification access anyway Log.d(TAG, "Foreground service failed to launch: $e") - val channel = NotificationChannel( - NotificationInfo.WearBluetooth.CRITICAL_ERROR_CHANNEL_ID, - context.getString(R.string.wear_bt_critical_error_channel_name), - NotificationManager.IMPORTANCE_HIGH - ).apply { setShowBadge(false) } - context.getSystemService(NotificationManager::class.java).createNotificationChannel(channel) - val notification = NotificationCompat.Builder(context, NotificationInfo.WearBluetooth.CRITICAL_ERROR_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notification_error) - .setContentTitle(context.getString(R.string.wear_bt_failed_foreground_notification_title)) - .setContentText(e.message) - .build() - with(NotificationManagerCompat.from(context)) { - if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS - ) != PackageManager.PERMISSION_GRANTED - ) { - return@with - } - notify( - NotificationInfo.WearBluetooth.CRITICAL_ERROR_NOTIFICATION_ID, - notification - ) - } + BluetoothServerService.showBluetoothServerError(context, context.getString(R.string.android_blocked_startup)) } } else { ContextCompat.startForegroundService( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a91c19d123..d6c01d3c28 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -395,4 +395,6 @@ Smartwatch support Failed to start Wear OS sync service Wear OS sync critical error + Android blocked starting the Wear OS service + Bluetooth is disabled From d210d6c89b0ca0108ea39f4af4af9fd671abcbfd Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 27 Sep 2026 17:21:52 +0200 Subject: [PATCH 2/4] Don't notify on Bluetooth being turned off We really need to handle Bluetooth better, but right now this will just spam users who have Bluetooth disabled --- .../java/protect/card_locker/wearos/BluetoothServerService.kt | 1 - app/src/main/res/values/strings.xml | 1 - 2 files changed, 2 deletions(-) diff --git a/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt b/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt index dee5589183..cc9a17b7f9 100644 --- a/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt +++ b/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt @@ -105,7 +105,6 @@ class BluetoothServerService : Service() { val adapter = (getSystemService(BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter if (adapter == null || !adapter.isEnabled) { Log.w(TAG, "Bluetooth not available or disabled") - showBluetoothServerError(this, getString(R.string.bluetooth_disabled)) stopSelf() return START_STICKY } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d6c01d3c28..1fa6b3c720 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -396,5 +396,4 @@ Failed to start Wear OS sync service Wear OS sync critical error Android blocked starting the Wear OS service - Bluetooth is disabled From a0c6e0463480936ffd59788357c179615684f973 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 27 Sep 2026 17:28:21 +0200 Subject: [PATCH 3/4] Sticky consistency with old setup --- .../java/protect/card_locker/wearos/BluetoothServerService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt b/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt index cc9a17b7f9..7a3e4e9b54 100644 --- a/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt +++ b/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt @@ -106,7 +106,7 @@ class BluetoothServerService : Service() { if (adapter == null || !adapter.isEnabled) { Log.w(TAG, "Bluetooth not available or disabled") stopSelf() - return START_STICKY + return START_NOT_STICKY } // Avoid tearing down a working accept socket every time Settings is resumed. From 1528ea23b641b91d9a8c874e3817ff0c0eb14140 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 27 Sep 2026 18:10:56 +0200 Subject: [PATCH 4/4] Cleanup --- .../protect/card_locker/wearos/WearSyncServiceManager.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/src/main/java/protect/card_locker/wearos/WearSyncServiceManager.kt b/app/src/main/java/protect/card_locker/wearos/WearSyncServiceManager.kt index 0d73b34f5a..9e91025c1d 100644 --- a/app/src/main/java/protect/card_locker/wearos/WearSyncServiceManager.kt +++ b/app/src/main/java/protect/card_locker/wearos/WearSyncServiceManager.kt @@ -2,22 +2,15 @@ package protect.card_locker.wearos import android.Manifest import android.app.ForegroundServiceStartNotAllowedException -import android.app.NotificationChannel -import android.app.NotificationManager import android.content.Context import android.content.Intent -import android.content.pm.PackageManager import android.os.Build import android.util.Log import androidx.activity.result.ActivityResultCaller import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.app.ActivityCompat -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.core.content.edit import androidx.preference.PreferenceManager -import protect.card_locker.NotificationInfo import protect.card_locker.R import protect.card_locker.preferences.Settings import protect.card_locker.shared.BluetoothPermissionHelper