From eed50283bb6d9d2d193a945858f17a18cc057bb9 Mon Sep 17 00:00:00 2001 From: Koen Date: Mon, 20 Nov 2023 10:18:43 +0100 Subject: [PATCH] Stability fixes. Better support for Android older than Oreo. Only ask for permissions once. --- .../com/futo/fcast/receiver/BootReceiver.kt | 72 +++++++++++++++++-- .../com/futo/fcast/receiver/FCastSession.kt | 2 +- .../com/futo/fcast/receiver/MainActivity.kt | 71 +++++++++++------- .../futo/fcast/receiver/TcpListenerService.kt | 42 ++++++----- .../app/src/main/res/values/strings.xml | 2 +- 5 files changed, 138 insertions(+), 51 deletions(-) diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/BootReceiver.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/BootReceiver.kt index 88131ea..f1d6618 100644 --- a/receivers/android/app/src/main/java/com/futo/fcast/receiver/BootReceiver.kt +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/BootReceiver.kt @@ -1,16 +1,78 @@ package com.futo.fcast.receiver +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.core.app.NotificationCompat class BootReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - if (intent.action == Intent.ACTION_BOOT_COMPLETED || - intent.action == Intent.ACTION_PACKAGE_ADDED || - intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) { - val serviceIntent = Intent(context, TcpListenerService::class.java) - context.startService(serviceIntent) + try { + if (intent.action == Intent.ACTION_BOOT_COMPLETED || + intent.action == Intent.ACTION_PACKAGE_ADDED || + intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Show a notification with an action to start the service + showStartServiceNotification(context); + } else { + // Directly start the service for older versions + val serviceIntent = Intent(context, TcpListenerService::class.java) + context.startService(serviceIntent) + } + } + } catch (e: Throwable) { + Log.e("BootReceiver", "Failed to start service", e) } } + + private fun createNotificationBuilder(context: Context): NotificationCompat.Builder { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationCompat.Builder(context, CHANNEL_ID) + } else { + // For pre-Oreo, do not specify the channel ID + NotificationCompat.Builder(context) + } + } + + private fun showStartServiceNotification(context: Context) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + // Create the Notification Channel for Android 8.0 and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channelName = "Service Start Channel" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(CHANNEL_ID, channelName, importance) + channel.description = "Notification Channel for Service Start" + notificationManager.createNotificationChannel(channel) + } + + // PendingIntent to start the TcpListenerService + val serviceIntent = Intent(context, TcpListenerService::class.java) + val pendingIntent = PendingIntent.getService(context, 0, serviceIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + val startServiceAction = NotificationCompat.Action.Builder(0, "Start Service", pendingIntent).build() + + // Build the notification + val notificationBuilder = createNotificationBuilder(context) + .setContentTitle("Start FCast Receiver Service") + .setContentText("Tap to start the service") + .setSmallIcon(R.mipmap.ic_launcher) + .addAction(startServiceAction) + .setAutoCancel(true) + + val notification = notificationBuilder.build() + + // Notify + notificationManager.notify(NOTIFICATION_ID, notification) + } + + companion object { + private const val CHANNEL_ID = "BootReceiverServiceChannel" + private const val NOTIFICATION_ID = 1 + } } \ No newline at end of file diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/FCastSession.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/FCastSession.kt index 4642c50..cef8405 100644 --- a/receivers/android/app/src/main/java/com/futo/fcast/receiver/FCastSession.kt +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/FCastSession.kt @@ -174,7 +174,7 @@ class FCastSession(private val _socket: Socket, private val _service: TcpListene Opcode.SetVolume -> _service.onSetVolume(Json.decodeFromString(body!!)) else -> { } } - } catch (e: Exception) { + } catch (e: Throwable) { Log.e(TAG, "Failed to handle packet (opcode: ${opcode}, body: '${body}')") } } diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/MainActivity.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/MainActivity.kt index b02edab..2f68936 100644 --- a/receivers/android/app/src/main/java/com/futo/fcast/receiver/MainActivity.kt +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/MainActivity.kt @@ -45,6 +45,7 @@ class MainActivity : AppCompatActivity() { private var _updating: Boolean = false private var _demoClickCount = 0 private var _lastDemoToast: Toast? = null + private val _preferenceFileKey get() = "$packageName.PREFERENCE_FILE_KEY" private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Main) @@ -172,7 +173,18 @@ class MainActivity : AppCompatActivity() { } if (listPermissionsNeeded.isNotEmpty()) { - ActivityCompat.requestPermissions(this, listPermissionsNeeded.toTypedArray(), REQUEST_ID_MULTIPLE_PERMISSIONS) + val permissionRequestedKey = "NOTIFICATIONS_PERMISSION_REQUESTED" + val sharedPref = this.getSharedPreferences(_preferenceFileKey, Context.MODE_PRIVATE) + val hasRequestedPermission = sharedPref.getBoolean(permissionRequestedKey, false) + if (!hasRequestedPermission) { + ActivityCompat.requestPermissions(this, listPermissionsNeeded.toTypedArray(), REQUEST_ID_MULTIPLE_PERMISSIONS) + with(sharedPref.edit()) { + putBoolean(permissionRequestedKey, true) + apply() + } + } else { + Toast.makeText(this, "Notifications permission missing", Toast.LENGTH_SHORT).show() + } return false } @@ -180,35 +192,42 @@ class MainActivity : AppCompatActivity() { } private fun requestSystemAlertWindowPermission() { - val preferenceFileKey = "$packageName.PREFERENCE_FILE_KEY" - val permissionRequestFailedKey = "SYSTEM_ALERT_WINDOW_PERMISSION_REQUESTED_FAILED_KEY" + try { + val permissionRequestedKey = "SYSTEM_ALERT_WINDOW_PERMISSION_REQUESTED" + val sharedPref = this.getSharedPreferences(_preferenceFileKey, Context.MODE_PRIVATE) + val hasRequestedPermission = sharedPref.getBoolean(permissionRequestedKey, false) - val sharedPref = this.getSharedPreferences(preferenceFileKey, Context.MODE_PRIVATE) - val hasPermissionRequestFailed = sharedPref.getBoolean(permissionRequestFailedKey, false) - - if (!hasPermissionRequestFailed && !Settings.canDrawOverlays(this)) { - AlertDialog.Builder(this) - .setTitle(R.string.permission_dialog_title) - .setMessage(R.string.permission_dialog_message) - .setPositiveButton(R.string.permission_dialog_positive_button) { _, _ -> - try { - val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")) - startActivityForResult(intent, REQUEST_CODE) - } catch (e: Exception) { - Log.e("OverlayPermission", "Error requesting overlay permission", e) - with(sharedPref.edit()) { - putBoolean(permissionRequestFailedKey, true) - apply() + if (!Settings.canDrawOverlays(this)) { + if (!hasRequestedPermission) { + AlertDialog.Builder(this) + .setTitle(R.string.permission_dialog_title) + .setMessage(R.string.permission_dialog_message) + .setPositiveButton(R.string.permission_dialog_positive_button) { _, _ -> + try { + val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")) + startActivityForResult(intent, REQUEST_CODE) + } catch (e: Throwable) { + Log.e("OverlayPermission", "Error requesting overlay permission", e) + Toast.makeText(this, "An error occurred: ${e.message}", Toast.LENGTH_LONG).show() + } } - Toast.makeText(this, "An error occurred: ${e.message}", Toast.LENGTH_LONG).show() + .setNegativeButton(R.string.permission_dialog_negative_button) { dialog, _ -> + dialog.dismiss() + Toast.makeText(this, "Permission is required to work in background", Toast.LENGTH_LONG).show() + } + .create() + .show() + + with(sharedPref.edit()) { + putBoolean(permissionRequestedKey, true) + apply() } + } else { + Toast.makeText(this, "Optional system alert window permission missing", Toast.LENGTH_SHORT).show() } - .setNegativeButton(R.string.permission_dialog_negative_button) { dialog, _ -> - dialog.dismiss() - Toast.makeText(this, "Permission is required to work in background", Toast.LENGTH_LONG).show() - } - .create() - .show() + } + } catch (e: Throwable) { + Log.e(TAG, "Failed to request system alert window permissions") } } diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/TcpListenerService.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/TcpListenerService.kt index 37e4c8d..40319d7 100644 --- a/receivers/android/app/src/main/java/com/futo/fcast/receiver/TcpListenerService.kt +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/TcpListenerService.kt @@ -40,11 +40,22 @@ class TcpListenerService : Service() { _scope = CoroutineScope(Dispatchers.Main) - createNotificationChannel() - val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = "TCP Listener Service" + val descriptionText = "Listening on port $PORT" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { + description = descriptionText + } + + val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + + val notification: Notification = createNotificationBuilder() .setContentTitle("TCP Listener Service") .setContentText("Listening on port $PORT") - .setSmallIcon(R.mipmap.ic_launcher) + .setSmallIcon(R.mipmap.ic_launcher) // Ensure this icon exists .build() startForeground(NOTIFICATION_ID, notification) @@ -98,6 +109,15 @@ class TcpListenerService : Service() { return START_STICKY } + private fun createNotificationBuilder(): NotificationCompat.Builder { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationCompat.Builder(this, CHANNEL_ID) + } else { + // For pre-Oreo, do not specify the channel ID + NotificationCompat.Builder(this) + } + } + override fun onDestroy() { super.onDestroy() @@ -168,7 +188,7 @@ class TcpListenerService : Service() { pi.send() } else { val pi = PendingIntent.getActivity(this@TcpListenerService, 0, i, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - val playNotification = NotificationCompat.Builder(this@TcpListenerService, CHANNEL_ID) + val playNotification = createNotificationBuilder() .setContentTitle("FCast") .setContentText("New content received. Tap to play.") .setSmallIcon(R.drawable.ic_launcher_background) @@ -312,20 +332,6 @@ class TcpListenerService : Service() { Log.i(TAG, "Disconnected ${socket.remoteSocketAddress}") } - private fun createNotificationChannel() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val name = "TCP Listener Service" - val descriptionText = "Listening on port $PORT" - val importance = NotificationManager.IMPORTANCE_DEFAULT - val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { - description = descriptionText - } - - val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(channel) - } - } - companion object { const val PORT = 46899 const val CHANNEL_ID = "TcpListenerServiceChannel" diff --git a/receivers/android/app/src/main/res/values/strings.xml b/receivers/android/app/src/main/res/values/strings.xml index f6dd6c2..aba6cf8 100644 --- a/receivers/android/app/src/main/res/values/strings.xml +++ b/receivers/android/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - FCastReceiver + FCast Receiver The operation failed in a generic way The operation failed because it was actively aborted The operation failed because it was blocked