1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-06-24 21:25:23 +00:00

Stability fixes. Better support for Android older than Oreo. Only ask for permissions once.

This commit is contained in:
Koen 2023-11-20 10:18:43 +01:00
parent 9322a89162
commit eed50283bb
5 changed files with 138 additions and 51 deletions

View file

@ -1,16 +1,78 @@
package com.futo.fcast.receiver package com.futo.fcast.receiver
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
class BootReceiver : BroadcastReceiver() { class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED || try {
intent.action == Intent.ACTION_PACKAGE_ADDED || if (intent.action == Intent.ACTION_BOOT_COMPLETED ||
intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) { intent.action == Intent.ACTION_PACKAGE_ADDED ||
val serviceIntent = Intent(context, TcpListenerService::class.java) intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) {
context.startService(serviceIntent)
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
}
} }

View file

@ -174,7 +174,7 @@ class FCastSession(private val _socket: Socket, private val _service: TcpListene
Opcode.SetVolume -> _service.onSetVolume(Json.decodeFromString(body!!)) Opcode.SetVolume -> _service.onSetVolume(Json.decodeFromString(body!!))
else -> { } else -> { }
} }
} catch (e: Exception) { } catch (e: Throwable) {
Log.e(TAG, "Failed to handle packet (opcode: ${opcode}, body: '${body}')") Log.e(TAG, "Failed to handle packet (opcode: ${opcode}, body: '${body}')")
} }
} }

View file

@ -45,6 +45,7 @@ class MainActivity : AppCompatActivity() {
private var _updating: Boolean = false private var _updating: Boolean = false
private var _demoClickCount = 0 private var _demoClickCount = 0
private var _lastDemoToast: Toast? = null private var _lastDemoToast: Toast? = null
private val _preferenceFileKey get() = "$packageName.PREFERENCE_FILE_KEY"
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Main) private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Main)
@ -172,7 +173,18 @@ class MainActivity : AppCompatActivity() {
} }
if (listPermissionsNeeded.isNotEmpty()) { 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 return false
} }
@ -180,35 +192,42 @@ class MainActivity : AppCompatActivity() {
} }
private fun requestSystemAlertWindowPermission() { private fun requestSystemAlertWindowPermission() {
val preferenceFileKey = "$packageName.PREFERENCE_FILE_KEY" try {
val permissionRequestFailedKey = "SYSTEM_ALERT_WINDOW_PERMISSION_REQUESTED_FAILED_KEY" 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) if (!Settings.canDrawOverlays(this)) {
val hasPermissionRequestFailed = sharedPref.getBoolean(permissionRequestFailedKey, false) if (!hasRequestedPermission) {
AlertDialog.Builder(this)
if (!hasPermissionRequestFailed && !Settings.canDrawOverlays(this)) { .setTitle(R.string.permission_dialog_title)
AlertDialog.Builder(this) .setMessage(R.string.permission_dialog_message)
.setTitle(R.string.permission_dialog_title) .setPositiveButton(R.string.permission_dialog_positive_button) { _, _ ->
.setMessage(R.string.permission_dialog_message) try {
.setPositiveButton(R.string.permission_dialog_positive_button) { _, _ -> val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))
try { startActivityForResult(intent, REQUEST_CODE)
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")) } catch (e: Throwable) {
startActivityForResult(intent, REQUEST_CODE) Log.e("OverlayPermission", "Error requesting overlay permission", e)
} catch (e: Exception) { Toast.makeText(this, "An error occurred: ${e.message}", Toast.LENGTH_LONG).show()
Log.e("OverlayPermission", "Error requesting overlay permission", e) }
with(sharedPref.edit()) {
putBoolean(permissionRequestFailedKey, true)
apply()
} }
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() } catch (e: Throwable) {
Toast.makeText(this, "Permission is required to work in background", Toast.LENGTH_LONG).show() Log.e(TAG, "Failed to request system alert window permissions")
}
.create()
.show()
} }
} }

View file

@ -40,11 +40,22 @@ class TcpListenerService : Service() {
_scope = CoroutineScope(Dispatchers.Main) _scope = CoroutineScope(Dispatchers.Main)
createNotificationChannel() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID) 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") .setContentTitle("TCP Listener Service")
.setContentText("Listening on port $PORT") .setContentText("Listening on port $PORT")
.setSmallIcon(R.mipmap.ic_launcher) .setSmallIcon(R.mipmap.ic_launcher) // Ensure this icon exists
.build() .build()
startForeground(NOTIFICATION_ID, notification) startForeground(NOTIFICATION_ID, notification)
@ -98,6 +109,15 @@ class TcpListenerService : Service() {
return START_STICKY 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() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
@ -168,7 +188,7 @@ class TcpListenerService : Service() {
pi.send() pi.send()
} else { } else {
val pi = PendingIntent.getActivity(this@TcpListenerService, 0, i, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) 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") .setContentTitle("FCast")
.setContentText("New content received. Tap to play.") .setContentText("New content received. Tap to play.")
.setSmallIcon(R.drawable.ic_launcher_background) .setSmallIcon(R.drawable.ic_launcher_background)
@ -312,20 +332,6 @@ class TcpListenerService : Service() {
Log.i(TAG, "Disconnected ${socket.remoteSocketAddress}") 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 { companion object {
const val PORT = 46899 const val PORT = 46899
const val CHANNEL_ID = "TcpListenerServiceChannel" const val CHANNEL_ID = "TcpListenerServiceChannel"

View file

@ -1,5 +1,5 @@
<resources> <resources>
<string name="app_name">FCastReceiver</string> <string name="app_name">FCast Receiver</string>
<string name="general_failure">The operation failed in a generic way</string> <string name="general_failure">The operation failed in a generic way</string>
<string name="aborted">The operation failed because it was actively aborted</string> <string name="aborted">The operation failed because it was actively aborted</string>
<string name="blocked">The operation failed because it was blocked</string> <string name="blocked">The operation failed because it was blocked</string>