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
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
}
}

View file

@ -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}')")
}
}

View file

@ -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")
}
}

View file

@ -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"

View file

@ -1,5 +1,5 @@
<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="aborted">The operation failed because it was actively aborted</string>
<string name="blocked">The operation failed because it was blocked</string>