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
|
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) {
|
||||||
|
try {
|
||||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED ||
|
if (intent.action == Intent.ACTION_BOOT_COMPLETED ||
|
||||||
intent.action == Intent.ACTION_PACKAGE_ADDED ||
|
intent.action == Intent.ACTION_PACKAGE_ADDED ||
|
||||||
intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) {
|
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)
|
val serviceIntent = Intent(context, TcpListenerService::class.java)
|
||||||
context.startService(serviceIntent)
|
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!!))
|
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}')")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()) {
|
||||||
|
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)
|
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,13 +192,13 @@ 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) {
|
||||||
|
|
||||||
if (!hasPermissionRequestFailed && !Settings.canDrawOverlays(this)) {
|
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle(R.string.permission_dialog_title)
|
.setTitle(R.string.permission_dialog_title)
|
||||||
.setMessage(R.string.permission_dialog_message)
|
.setMessage(R.string.permission_dialog_message)
|
||||||
|
@ -194,12 +206,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
try {
|
try {
|
||||||
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))
|
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))
|
||||||
startActivityForResult(intent, REQUEST_CODE)
|
startActivityForResult(intent, REQUEST_CODE)
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
Log.e("OverlayPermission", "Error requesting overlay permission", e)
|
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()
|
Toast.makeText(this, "An error occurred: ${e.message}", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -209,6 +217,17 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
.create()
|
.create()
|
||||||
.show()
|
.show()
|
||||||
|
|
||||||
|
with(sharedPref.edit()) {
|
||||||
|
putBoolean(permissionRequestedKey, true)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "Optional system alert window permission missing", Toast.LENGTH_SHORT).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)
|
_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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue