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:
parent
9322a89162
commit
eed50283bb
5 changed files with 138 additions and 51 deletions
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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}')")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue