aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
authorBlaster4385 <[email protected]>2025-05-03 13:52:44 +0530
committerBlaster4385 <[email protected]>2025-05-05 21:29:11 +0530
commitb9fd819e63644eaa3d77595c9aec507cb4b2bfc4 (patch)
treef090cd959066f20b4a8223ea098503db9307bd1f /app/src/main/java
parentc9fecb147b277d3b43b7d2a4e25ce3b8574febb2 (diff)
feat: added foreground service to maintain mqtt connection in background
- Handle all mqtt stuff in the service. - Convert BatterySensorService to a data provider.
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/dev/tablaster/dashpanel/DashPanelService.kt349
-rw-r--r--app/src/main/java/dev/tablaster/dashpanel/MainActivity.kt238
-rw-r--r--app/src/main/java/dev/tablaster/dashpanel/mqtt/ConnectionData.kt34
-rw-r--r--app/src/main/java/dev/tablaster/dashpanel/mqtt/MQTTModule.kt8
-rw-r--r--app/src/main/java/dev/tablaster/dashpanel/mqtt/MQTTService.kt2
-rw-r--r--app/src/main/java/dev/tablaster/dashpanel/sensor/BatterySensorProvider.kt222
-rw-r--r--app/src/main/java/dev/tablaster/dashpanel/sensor/BatterySensorService.kt201
-rw-r--r--app/src/main/java/dev/tablaster/dashpanel/sensor/SensorProvider.kt27
-rw-r--r--app/src/main/java/dev/tablaster/dashpanel/utils/NotificationUtils.kt75
9 files changed, 817 insertions, 339 deletions
diff --git a/app/src/main/java/dev/tablaster/dashpanel/DashPanelService.kt b/app/src/main/java/dev/tablaster/dashpanel/DashPanelService.kt
new file mode 100644
index 0000000..06ef35c
--- /dev/null
+++ b/app/src/main/java/dev/tablaster/dashpanel/DashPanelService.kt
@@ -0,0 +1,349 @@
+package dev.tablaster.dashpanel
+
+import android.app.Service
+import android.content.Intent
+import android.os.Binder
+import android.os.Build
+import android.os.Handler
+import android.os.IBinder
+import android.os.Looper
+import android.os.PowerManager
+import android.util.Log
+import android.content.SharedPreferences
+import androidx.lifecycle.ProcessLifecycleOwner
+import androidx.preference.PreferenceManager
+import dev.tablaster.dashpanel.mqtt.ConnectionData
+import dev.tablaster.dashpanel.mqtt.MQTTModule
+import dev.tablaster.dashpanel.mqtt.MQTTOptions
+import dev.tablaster.dashpanel.sensor.BatterySensorProvider
+import dev.tablaster.dashpanel.sensor.SensorProvider
+import dev.tablaster.dashpanel.utils.NotificationUtils
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+import org.json.JSONObject
+import java.util.concurrent.ConcurrentHashMap
+
+class DashPanelService : Service(), MQTTModule.MQTTListener {
+ private val TAG = "DashPanelService"
+
+ private lateinit var mqttOptions: MQTTOptions
+ private lateinit var mqttModule: MQTTModule
+ private lateinit var connectionData: ConnectionData
+ private lateinit var notificationUtils: NotificationUtils
+
+ private lateinit var wakeLock: PowerManager.WakeLock
+ private var wakeLockAcquired = false
+
+ private val handler = Handler(Looper.getMainLooper())
+ private var updateInterval = 15
+
+ private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
+ private val providers = ConcurrentHashMap<String, SensorProvider>()
+ private val providerInitialized = ConcurrentHashMap<String, Boolean>()
+
+ private val pendingValues = ConcurrentHashMap<String, String>()
+
+ private val binder = LocalBinder()
+ private var sensorsInitialized = false
+ private var isConnected = false
+
+ private val updateRunnable = object : Runnable {
+ override fun run() {
+ updateAllSensors()
+ handler.postDelayed(this, updateInterval * 60 * 1000L)
+ }
+ }
+
+ private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
+ Log.d(TAG, "SharedPreference changed: $key")
+ when (key) {
+ in mqttKeys -> {
+ val newOptions = MQTTOptions(sharedPreferences)
+ if (newOptions.isValid) {
+ Log.d(TAG, "MQTT settings changed and are now valid. Restarting MQTT.")
+ mqttModule.stop()
+ mqttOptions = newOptions
+ mqttModule = MQTTModule(this, mqttOptions, this)
+ mqttModule.start()
+ } else {
+ Log.d(TAG, "MQTT settings changed but are still invalid.")
+ mqttModule.stop()
+ }
+ }
+ "sensor_update_interval" -> {
+ val newInterval = sharedPreferences.getString("sensor_update_interval", "15")!!.toInt()
+ if (newInterval != updateInterval) {
+ Log.d(TAG, "Update interval changed from $updateInterval to $newInterval minutes")
+ updateInterval = newInterval
+ restartPeriodicUpdates()
+ }
+ }
+ }
+ }
+
+ private val mqttKeys = setOf(
+ "mqtt_enabled", "mqtt_broker", "mqtt_port", "mqtt_client_id",
+ "mqtt_username", "mqtt_password", "mqtt_tls", "mqtt_base_topic"
+ )
+
+ inner class LocalBinder : Binder() {
+ fun getService(): DashPanelService = this@DashPanelService
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ val powerManager = getSystemService(POWER_SERVICE) as PowerManager
+ wakeLock = powerManager.newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK or
+ PowerManager.ON_AFTER_RELEASE,
+ "DashPanel:MQTTServiceWakeLock"
+ )
+ wakeLock.setReferenceCounted(false)
+
+ notificationUtils = NotificationUtils(this)
+ notificationUtils.startForegroundService()
+ Log.d(TAG, "DashPanelService started")
+
+ val prefs = PreferenceManager.getDefaultSharedPreferences(this)
+ mqttOptions = MQTTOptions(prefs)
+ mqttModule = MQTTModule(this, mqttOptions, this)
+
+ updateInterval = prefs.getString("sensor_update_interval", "15")!!.toInt()
+
+ prefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
+
+ connectionData = ConnectionData(this)
+ connectionData.observe(ProcessLifecycleOwner.get()) { connected ->
+ if (connected && !isConnected) {
+ mqttModule.restart()
+ isConnected = true
+ } else if (!connected && isConnected) {
+ mqttModule.pause()
+ isConnected = false
+ }
+ }
+ mqttModule.start()
+
+ if (!sensorsInitialized) {
+ registerSensorProvider(BatterySensorProvider(this))
+ sensorsInitialized = true
+ }
+
+ startPeriodicUpdates()
+ }
+
+ private fun startPeriodicUpdates() {
+ Log.d(TAG, "Starting periodic updates with interval: $updateInterval minutes")
+ handler.removeCallbacks(updateRunnable)
+ handler.postDelayed(updateRunnable, updateInterval * 60 * 1000L)
+ }
+
+ private fun restartPeriodicUpdates() {
+ Log.d(TAG, "Restarting periodic updates with new interval: $updateInterval minutes")
+ handler.removeCallbacks(updateRunnable)
+ handler.postDelayed(updateRunnable, updateInterval * 60 * 1000L)
+ }
+
+ private fun updateAllSensors() {
+ Log.d(TAG, "Performing scheduled update of all sensors")
+ providers.values.forEach { provider ->
+ Log.d(TAG, "Updating sensor: ${provider.javaClass.simpleName}")
+ provider.updateSensorData()
+ }
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ acquireWakeLock()
+ return START_STICKY
+ }
+
+ override fun onBind(intent: Intent): IBinder = binder
+
+ override fun onDestroy() {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(this)
+ prefs.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
+
+ publishAvailability(false)
+ notificationUtils.stopForegroundService()
+ mqttModule.stop()
+ providers.values.forEach { it.stop() }
+ providers.clear()
+ connectionData.cleanup()
+
+ handler.removeCallbacksAndMessages(null)
+
+ releaseWakeLock()
+ super.onDestroy()
+ }
+
+ private fun acquireWakeLock() {
+ if (!wakeLockAcquired) {
+ Log.d(TAG, "Acquiring wake lock")
+ wakeLock.acquire(60*60*1000L)
+ wakeLockAcquired = true
+ }
+ }
+
+ private fun releaseWakeLock() {
+ if (wakeLockAcquired) {
+ Log.d(TAG, "Releasing wake lock")
+ if (wakeLock.isHeld) {
+ wakeLock.release()
+ }
+ wakeLockAcquired = false
+ }
+ }
+
+ private fun registerSensorProvider(provider: SensorProvider) {
+ val id = provider.javaClass.simpleName
+ providers[id] = provider
+ provider.setDataListener(object : SensorProvider.DataListener {
+ override fun onSensorData(type: String, value: String) {
+ publishOrCache(type, value)
+ }
+ })
+
+ if (mqttModule.isReady() == true) {
+ publishDiscoveries(provider)
+ provider.onMqttConnected()
+ providerInitialized[id] = true
+ }
+ }
+
+ private fun publishOrCache(type: String, value: String) {
+ if (mqttModule.isReady() == true) {
+ publishSensorData(type, value)
+ } else {
+ Log.d(TAG, "Caching sensor data for $type: $value")
+ pendingValues[type] = value
+ }
+ }
+
+ private fun publishSensorData(type: String, value: String) {
+ var prefix = "sensor"
+ var found = false
+
+ for (provider in providers.values) {
+ for (cfg in provider.getSensorConfigurations()) {
+ if (cfg.idSuffix == type) {
+ prefix = cfg.prefix
+ found = true
+ break
+ }
+ }
+ if (found) break
+ }
+
+ val topic = "${mqttOptions.getBaseTopic()}/$prefix/${Build.MODEL}/$type/state"
+ Log.d(TAG, "Publishing sensor data for $type: $value to $topic")
+ mqttModule.publish(topic, value, retain = true)
+ }
+
+ private fun publishDiscoveries(provider: SensorProvider) {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(this)
+ val interval = prefs.getString("sensor_update_interval", "15")!!.toInt()
+
+ provider.getSensorConfigurations().forEach { cfg ->
+ val payload = JSONObject().apply {
+ put("name", "${Build.MODEL} ${cfg.nameSuffix}")
+ put("unique_id", "${Build.MODEL}_${cfg.idSuffix}")
+ cfg.deviceClass?.let { dc -> put("device_class", dc) }
+ put("state_topic", "${mqttOptions.getBaseTopic()}/${cfg.prefix}/${Build.MODEL}/${cfg.idSuffix}/state")
+ put("availability_topic", "${mqttOptions.getBaseTopic()}/dashpanel/connection")
+ cfg.unit?.let { u -> put("unit_of_measurement", u) }
+ cfg.valueTemplate?.let { vt -> put("value_template", vt) }
+ put("expire_after", interval * 60 + 30)
+ put("device", JSONObject().apply {
+ put("identifiers", listOf(Build.DEVICE))
+ put("name", Build.MODEL)
+ put("manufacturer", Build.MANUFACTURER ?: "Unknown")
+ put("model", Build.MODEL)
+ put("sw_version", Build.VERSION.RELEASE)
+ })
+ if (cfg.binary) {
+ put("payload_on", "ON")
+ put("payload_off", "OFF")
+ }
+ }
+
+ val topic = "${mqttOptions.getBaseTopic()}/${cfg.prefix}/${Build.MODEL}/${cfg.idSuffix}/config"
+ Log.d(TAG, "Publishing discovery for ${cfg.idSuffix} to $topic")
+ mqttModule.publish(topic, payload.toString(), retain = true)
+ }
+ }
+
+ private fun publishAllDiscoveries() {
+ providers.values.forEach { provider ->
+ publishDiscoveries(provider)
+ }
+ }
+
+ private fun publishPendingValues() {
+ if (pendingValues.isNotEmpty()) {
+ Log.d(TAG, "Publishing ${pendingValues.size} pending sensor values")
+ pendingValues.forEach { (key, value) ->
+ publishSensorData(key, value)
+ }
+ pendingValues.clear()
+ }
+ }
+
+ private fun publishAvailability(online: Boolean) {
+ if (mqttModule.isReady() != true) return
+ val status = if (online) "online" else "offline"
+ mqttModule.publish("${mqttOptions.getBaseTopic()}/dashpanel/connection", status, retain = true)
+ }
+
+ override fun onMQTTConnect() {
+ scope.launch {
+ notificationUtils.updateNotification(
+ getString(R.string.service_notification_title),
+ "Connected to MQTT broker"
+ )
+
+ publishAvailability(true)
+
+ publishAllDiscoveries()
+
+ publishPendingValues()
+
+ providers.forEach { (id, provider) ->
+ if (providerInitialized[id] != true) {
+ provider.onMqttConnected()
+ providerInitialized[id] = true
+ }
+ }
+ updateAllSensors()
+ }
+ }
+
+ override fun onMQTTDisconnect() {
+ scope.launch {
+ notificationUtils.updateNotification(
+ getString(R.string.service_notification_title),
+ "Disconnected from MQTT broker"
+ )
+
+ if (isConnected) {
+ Log.d(TAG, "Attempting to reconnect to MQTT broker after disconnect")
+ mqttModule.restart()
+ }
+ }
+ }
+
+ override fun onMQTTException(message: String) {
+ scope.launch {
+ notificationUtils.updateNotification(
+ getString(R.string.service_notification_title),
+ "Error: $message"
+ )
+ }
+ }
+
+ override fun onMQTTMessage(id: String, topic: String, payload: String) {
+ // TODO Handle incoming mqtt commands
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/dev/tablaster/dashpanel/MainActivity.kt b/app/src/main/java/dev/tablaster/dashpanel/MainActivity.kt
index 38a7fbd..e575f36 100644
--- a/app/src/main/java/dev/tablaster/dashpanel/MainActivity.kt
+++ b/app/src/main/java/dev/tablaster/dashpanel/MainActivity.kt
@@ -1,10 +1,18 @@
package dev.tablaster.dashpanel
import android.annotation.SuppressLint
+import android.content.ComponentName
+import android.content.Context
import android.content.Intent
+import android.content.ServiceConnection
import android.content.SharedPreferences
+import android.content.pm.PackageManager
import android.net.Uri
+import android.os.Build
import android.os.Bundle
+import android.os.IBinder
+import android.os.PowerManager
+import android.provider.Settings
import android.util.Log
import android.view.View
import android.view.WindowManager
@@ -15,86 +23,76 @@ import android.webkit.WebViewClient
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.core.net.toUri
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
-import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.google.android.material.floatingactionbutton.FloatingActionButton
-import dev.tablaster.dashpanel.mqtt.ConnectionData
-import dev.tablaster.dashpanel.mqtt.MQTTModule
-import dev.tablaster.dashpanel.mqtt.MQTTOptions
-import dev.tablaster.dashpanel.sensor.BatterySensorService
-import kotlinx.coroutines.launch
-import androidx.core.net.toUri
-class MainActivity : AppCompatActivity(), MQTTModule.MQTTListener {
+class MainActivity : AppCompatActivity() {
+ private val TAG = "MainActivity"
private lateinit var webView: WebView
private lateinit var settingsButton: FloatingActionButton
- private lateinit var mqttOptions: MQTTOptions
- private lateinit var mqttModule: MQTTModule
- private lateinit var connectionData: ConnectionData
- private var batterySensorService: BatterySensorService? = null
-
private lateinit var sharedPref: SharedPreferences
+ private var dashPanelService: DashPanelService? = null
+ private var isServiceBound = false
+
+ private val connection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ Log.d(TAG, "Service connected")
+ dashPanelService = (service as DashPanelService.LocalBinder).getService()
+ isServiceBound = true
+ }
+ override fun onServiceDisconnected(name: ComponentName?) {
+ Log.d(TAG, "Service disconnected")
+ dashPanelService = null
+ isServiceBound = false
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ checkBatteryOptimization()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+ requestPermissions(arrayOf(android.Manifest.permission.POST_NOTIFICATIONS), 1001)
+ }
+ }
sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
setupScreen()
setContentView(R.layout.activity_main)
- initializeMqtt()
-
- connectionData = ConnectionData(this)
- connectionData.observe(this) { isConnected ->
- if (isConnected) {
- Log.d("MainActivity", "Network connected.")
- mqttModule.restart()
- } else {
- Log.d("MainActivity", "Network disconnected.")
- mqttModule.pause()
- }
+ Intent(this, DashPanelService::class.java).also {
+ ContextCompat.startForegroundService(this, it)
+ bindService(it, connection, Context.BIND_AUTO_CREATE)
}
}
- override fun onWindowFocusChanged(hasFocus: Boolean) {
- val settingsPassCodePref = sharedPref.getInt("settings_passcode", 0)
- settingsButton = findViewById(R.id.settings_button)
- settingsButton.setOnClickListener { openSettings(settingsPassCodePref) }
- val url = sharedPref.getString("dashboard_url", "")
- if (!url.isNullOrBlank()) {
- setupWebView(url)
- } else {
- openSettings(0)
- }
- super.onWindowFocusChanged(hasFocus)
- if (hasFocus) {
- setupScreen()
- handleSensors()
+ override fun onStart() {
+ super.onStart()
+ if (!isServiceBound) {
+ Intent(this, DashPanelService::class.java).also {
+ ContextCompat.startForegroundService(this, it)
+ bindService(it, connection, Context.BIND_AUTO_CREATE)
+ }
}
}
- private fun initializeMqtt() {
- mqttOptions = MQTTOptions(sharedPref)
- mqttModule = MQTTModule(this, mqttOptions, this)
- lifecycle.addObserver(mqttModule)
+ override fun onStop() {
+ super.onStop()
+ if (isServiceBound) {
+ unbindService(connection)
+ isServiceBound = false
+ }
}
- private fun handleSensors() {
- val sensorBatteryPref = sharedPref.getBoolean("sensor_battery", false)
- val updateInterval = sharedPref.getString("sensor_update_interval", "15")?.toIntOrNull() ?: 15
-
- if (sensorBatteryPref) {
- if (batterySensorService == null) {
- batterySensorService = BatterySensorService(this, mqttModule, mqttOptions, updateInterval)
- lifecycle.addObserver(batterySensorService!!)
- }
- } else {
- batterySensorService?.stop()
- batterySensorService = null
- }
+ override fun onDestroy() {
+ super.onDestroy()
+ if (isServiceBound) unbindService(connection)
}
@SuppressLint("SetJavaScriptEnabled")
@@ -102,107 +100,87 @@ class MainActivity : AppCompatActivity(), MQTTModule.MQTTListener {
val hardwareAcceleration = sharedPref.getBoolean("hardware_acceleration", true)
if (!::webView.isInitialized) {
webView = findViewById(R.id.webview)
- val webSettings = webView.settings
- webSettings.javaScriptEnabled = true
- webSettings.domStorageEnabled = true
- webSettings.javaScriptCanOpenWindowsAutomatically = true
- webSettings.cacheMode = WebSettings.LOAD_NO_CACHE
- webSettings.allowContentAccess = true
- webSettings.allowFileAccess = true
- webSettings.setSupportZoom(true)
- webSettings.loadWithOverviewMode = true
- webSettings.useWideViewPort = true
- webSettings.mediaPlaybackRequiresUserGesture = false
+ with(webView.settings) {
+ javaScriptEnabled = true
+ domStorageEnabled = true
+ javaScriptCanOpenWindowsAutomatically = true
+ cacheMode = WebSettings.LOAD_NO_CACHE
+ allowContentAccess = true
+ allowFileAccess = true
+ setSupportZoom(true)
+ loadWithOverviewMode = true
+ useWideViewPort = true
+ mediaPlaybackRequiresUserGesture = false
+ }
webView.webViewClient = WebViewClient()
webView.webChromeClient = WebChromeClient()
}
- if (hardwareAcceleration) {
- webView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
- }
+ if (hardwareAcceleration) webView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
val currentUrl = webView.url
val currentOrigin = currentUrl?.toUri()?.origin()
val targetOrigin = url.toUri().origin()
-
if (currentOrigin != targetOrigin) {
- Log.d("MainActivity", "WebView origin changed: $currentOrigin -> $targetOrigin. Reloading.")
+ Log.d(TAG, "WebView origin changed: $currentOrigin -> $targetOrigin. Reloading.")
webView.loadUrl(url)
- } else {
- Log.d("MainActivity", "WebView already showing the same origin. Skipping reload.")
- }
- }
-
- private fun Uri.origin(): String {
- val portPart = if (port != -1 && port != defaultPortForScheme()) ":$port" else ""
- return "$scheme://$host$portPart"
- }
-
- private fun Uri.defaultPortForScheme(): Int {
- return when (scheme) {
- "http" -> 80
- "https" -> 443
- else -> -1
- }
- }
-
- private fun openSettings(settingsPassCodePreference: Int) {
- if (settingsPassCodePreference != 0) {
- val passCodeDialog = PassCodeFragment { enteredCode ->
- if (enteredCode == settingsPassCodePreference) {
- startActivity(Intent(this, SettingsActivity::class.java))
- } else {
- Toast.makeText(this, "Incorrect passcode", Toast.LENGTH_SHORT).show()
- }
- }
- passCodeDialog.show(supportFragmentManager, "SettingsPassCode")
- } else {
- startActivity(Intent(this, SettingsActivity::class.java))
}
}
private fun setupScreen() {
val fullScreenPref = sharedPref.getBoolean("full_screen", false)
val screenOnPref = sharedPref.getBoolean("screen_on", false)
-
if (fullScreenPref) {
enableEdgeToEdge()
- val windowInsetsController =
- WindowCompat.getInsetsController(window, window.decorView)
- windowInsetsController.systemBarsBehavior =
- WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
- windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
- }
-
- if (screenOnPref) {
- window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ WindowCompat.getInsetsController(window, window.decorView).apply {
+ systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ hide(WindowInsetsCompat.Type.systemBars())
+ }
}
+ if (screenOnPref) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
- override fun onMQTTConnect() {
- Log.d("MainActivity", "MQTT Connected")
- lifecycleScope.launch {
- Toast.makeText(this@MainActivity, "Connected to MQTT broker", Toast.LENGTH_SHORT).show()
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
+ super.onWindowFocusChanged(hasFocus)
+ settingsButton = findViewById(R.id.settings_button)
+ val passCodePref = sharedPref.getInt("settings_passcode", 0)
+ settingsButton.setOnClickListener {
+ if (passCodePref != 0) {
+ PassCodeFragment { code ->
+ if (code == passCodePref) startActivity(Intent(this, SettingsActivity::class.java))
+ else Toast.makeText(this, "Incorrect passcode", Toast.LENGTH_SHORT).show()
+ }.show(supportFragmentManager, "SettingsPassCode")
+ } else {
+ startActivity(Intent(this, SettingsActivity::class.java))
+ }
}
- batterySensorService?.onMqttConnected()
- }
+ val url = sharedPref.getString("dashboard_url", "")
+ if (!url.isNullOrBlank()) setupWebView(url)
+ else startActivity(Intent(this, SettingsActivity::class.java))
- override fun onMQTTDisconnect() {
- Log.d("MainActivity", "MQTT Disconnected")
- lifecycleScope.launch {
- Toast.makeText(this@MainActivity, "Disconnected from MQTT broker", Toast.LENGTH_SHORT).show()
- }
+ if (hasFocus) setupScreen()
}
- override fun onMQTTException(message: String) {
- Log.e("MainActivity", "MQTT Error: $message")
- lifecycleScope.launch {
- Toast.makeText(this@MainActivity, "MQTT Error: $message", Toast.LENGTH_LONG).show()
+ @SuppressLint("BatteryLife")
+ private fun checkBatteryOptimization() {
+ val powerManager = getSystemService(POWER_SERVICE) as PowerManager
+ val packageName = packageName
+
+ if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
+ val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
+ intent.data = "package:$packageName".toUri()
+ startActivity(intent)
}
}
+}
- override fun onMQTTMessage(id: String, topic: String, payload: String) {
- Log.d("MainActivity", "MQTT Message - Topic: $topic, Payload: $payload")
- // TODO Handle incoming mqtt commands.
- }
+private fun Uri.origin(): String {
+ val portPart = if (port != -1 && port != defaultPortForScheme()) ":$port" else ""
+ return "$scheme://$host$portPart"
}
+
+private fun Uri.defaultPortForScheme(): Int = when (scheme) {
+ "http" -> 80
+ "https" -> 443
+ else -> -1
+} \ No newline at end of file
diff --git a/app/src/main/java/dev/tablaster/dashpanel/mqtt/ConnectionData.kt b/app/src/main/java/dev/tablaster/dashpanel/mqtt/ConnectionData.kt
index 34430b0..e346fe0 100644
--- a/app/src/main/java/dev/tablaster/dashpanel/mqtt/ConnectionData.kt
+++ b/app/src/main/java/dev/tablaster/dashpanel/mqtt/ConnectionData.kt
@@ -4,6 +4,7 @@ import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
+import android.net.NetworkRequest
import android.util.Log
import androidx.lifecycle.LiveData
@@ -24,31 +25,48 @@ class ConnectionData(private val context: Context) : LiveData<Boolean>() {
}
}
- override fun onActive() {
- super.onActive()
- Log.d("ConnectionData", "Registering network callback.")
+ init {
+ registerNetworkCallback()
postValue(isConnected())
+ }
+
+ private fun registerNetworkCallback() {
try {
- connectivityManager.registerDefaultNetworkCallback(networkCallback)
+ val request = NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build()
+
+ connectivityManager.registerNetworkCallback(request, networkCallback)
+ Log.d("ConnectionData", "Network callback registered permanently")
} catch (e: Exception) {
Log.e("ConnectionData", "Error registering network callback", e)
}
}
- override fun onInactive() {
- super.onInactive()
- Log.d("ConnectionData", "Unregistering network callback.")
+ fun cleanup() {
try {
connectivityManager.unregisterNetworkCallback(networkCallback)
+ Log.d("ConnectionData", "Network callback unregistered during cleanup")
} catch (e: Exception) {
Log.e("ConnectionData", "Error unregistering network callback", e)
}
}
+ override fun onActive() {
+ super.onActive()
+ Log.d("ConnectionData", "LiveData observer active")
+ postValue(isConnected())
+ }
+
+ override fun onInactive() {
+ super.onInactive()
+ Log.d("ConnectionData", "LiveData observer inactive - keeping network callback registered")
+ }
+
private fun isConnected(): Boolean {
val network = connectivityManager.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
-}
+} \ No newline at end of file
diff --git a/app/src/main/java/dev/tablaster/dashpanel/mqtt/MQTTModule.kt b/app/src/main/java/dev/tablaster/dashpanel/mqtt/MQTTModule.kt
index f90bfb7..60e8f82 100644
--- a/app/src/main/java/dev/tablaster/dashpanel/mqtt/MQTTModule.kt
+++ b/app/src/main/java/dev/tablaster/dashpanel/mqtt/MQTTModule.kt
@@ -63,6 +63,14 @@ class MQTTModule(
return mqttService?.isReady
}
+ fun start() {
+ startMqtt()
+ }
+
+ fun stop() {
+ stopMqtt()
+ }
+
fun restart() {
Log.d(TAG, "restart")
stopMqtt()
diff --git a/app/src/main/java/dev/tablaster/dashpanel/mqtt/MQTTService.kt b/app/src/main/java/dev/tablaster/dashpanel/mqtt/MQTTService.kt
index 28ae1d6..8671fb3 100644
--- a/app/src/main/java/dev/tablaster/dashpanel/mqtt/MQTTService.kt
+++ b/app/src/main/java/dev/tablaster/dashpanel/mqtt/MQTTService.kt
@@ -255,6 +255,7 @@ class MQTTService(
val clientConnect = mqtt3AsyncClient!!.connectWith()
.cleanSession(false)
+ .keepAlive(30)
.willPublish()
.topic("${mqttOptions.getBaseTopic()}/dashpanel/$CONNECTION")
.payload(OFFLINE.toByteArray())
@@ -329,6 +330,7 @@ class MQTTService(
val clientConnect = mqtt5AsyncClient!!.connectWith()
.cleanStart(false)
+ .keepAlive(30)
.willPublish()
.topic("${mqttOptions.getBaseTopic()}/dashpanel/$CONNECTION")
.payload(OFFLINE.toByteArray())
diff --git a/app/src/main/java/dev/tablaster/dashpanel/sensor/BatterySensorProvider.kt b/app/src/main/java/dev/tablaster/dashpanel/sensor/BatterySensorProvider.kt
new file mode 100644
index 0000000..8054ca3
--- /dev/null
+++ b/app/src/main/java/dev/tablaster/dashpanel/sensor/BatterySensorProvider.kt
@@ -0,0 +1,222 @@
+package dev.tablaster.dashpanel.sensor
+
+import android.content.*
+import android.os.*
+import android.util.Log
+import androidx.preference.PreferenceManager
+
+class BatterySensorProvider(
+ base: Context
+) : ContextWrapper(base), SensorProvider {
+ private val TAG = "BatterySensorProvider"
+ private var dataListener: SensorProvider.DataListener? = null
+ private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
+ private var isEnabled = true
+
+ private var lastBatteryLevel = -1
+ private var lastBatteryTemperature = -1f
+ private var lastChargingState = false
+
+ private var pendingBatteryLevel: Int? = null
+ private var pendingBatteryTemperature: Float? = null
+ private var pendingChargingState: Boolean? = null
+
+ private val receiver = object : BroadcastReceiver() {
+ override fun onReceive(ctx: Context?, intent: Intent?) {
+ intent?.takeIf { it.action == Intent.ACTION_BATTERY_CHANGED }
+ ?.let { updateBatteryStates(it) }
+ }
+ }
+
+ private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { prefs, key ->
+ if (key == "sensor_battery") {
+ val newState = prefs.getBoolean(key, true)
+ if (newState != isEnabled) {
+ Log.d(TAG, "Battery sensor state changing from $isEnabled to $newState")
+ isEnabled = newState
+
+ if (isEnabled) {
+ Log.d(TAG, "Re-enabling battery sensor provider")
+ try {
+ registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to register receiver: ${e.message}")
+ }
+ forceUpdateBatteryStates()
+ } else {
+ Log.d(TAG, "Disabling battery sensor provider")
+ try {
+ unregisterReceiver(receiver)
+ } catch (e: IllegalArgumentException) {
+ Log.w(TAG, "Receiver not registered: ${e.message}")
+ }
+ notifyProvidersDisabled()
+ }
+ }
+ }
+ }
+
+ init {
+ Log.d(TAG, "Initializing BatterySensorProvider")
+ isEnabled = sharedPreferences.getBoolean("sensor_battery", true)
+ sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
+
+ if (isEnabled) {
+ try {
+ registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
+ } catch (e: Exception) {
+ Log.e(TAG, "Error during initialization: ${e.message}")
+ }
+ } else {
+ Log.d(TAG, "Battery sensor provider disabled at initialization")
+ }
+ }
+
+ override fun updateSensorData() {
+ if (isEnabled) {
+ forceUpdateBatteryStates()
+ }
+ }
+
+ private fun forceUpdateBatteryStates() {
+ if (!isEnabled) return
+ registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))?.let {
+ updateBatteryStates(it, true)
+ }
+ }
+
+ private fun updateBatteryStates(intent: Intent, force: Boolean = false) {
+ handleBatteryLevel(intent, force)
+ handleBatteryTemperature(intent, force)
+ handleChargingState(intent, force)
+ }
+
+ private fun handleBatteryLevel(intent: Intent, force: Boolean = false) {
+ val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
+ val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)
+ if (level >= 0 && scale > 0) {
+ val batteryPct = (level * 100) / scale
+ if (force || batteryPct != lastBatteryLevel) {
+ lastBatteryLevel = batteryPct
+ Log.d(TAG, "Battery level changed to $batteryPct%")
+ emitOrCache("battery_level", batteryPct.toString()) { pendingBatteryLevel = batteryPct }
+ }
+ }
+ }
+
+ private fun handleBatteryTemperature(intent: Intent, force: Boolean = false) {
+ val temperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1)
+ if (temperature > 0) {
+ val tempCelsius = temperature / 10.0f
+ if (force || tempCelsius != lastBatteryTemperature) {
+ lastBatteryTemperature = tempCelsius
+ Log.d(TAG, "Battery temperature changed to $tempCelsius°C")
+ emitOrCache("battery_temperature", String.format("%.1f", tempCelsius)) {
+ pendingBatteryTemperature = tempCelsius
+ }
+ }
+ }
+ }
+
+ private fun handleChargingState(intent: Intent, force: Boolean = false) {
+ val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
+ val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
+ status == BatteryManager.BATTERY_STATUS_FULL
+ if (force || isCharging != lastChargingState) {
+ lastChargingState = isCharging
+ Log.d(TAG, "Charging state changed to ${if (isCharging) "ON" else "OFF"}")
+ emitOrCache("charging", if (isCharging) "ON" else "OFF") {
+ pendingChargingState = isCharging
+ }
+ }
+ }
+
+ private fun emitOrCache(sensorId: String, value: String, cache: () -> Unit) {
+ if (dataListener != null) {
+ dataListener?.onSensorData(sensorId, value)
+ } else {
+ cache()
+ }
+ }
+
+ private fun notifyProvidersDisabled() {
+ dataListener?.let {
+ Log.d(TAG, "Notifying that all sensors are unavailable")
+ it.onSensorData("battery_level", "unavailable")
+ it.onSensorData("battery_temperature", "unavailable")
+ it.onSensorData("charging", "unavailable")
+ }
+ }
+
+ override fun getSensorConfigurations(): List<SensorProvider.SensorConfiguration> = listOf(
+ SensorProvider.SensorConfiguration(
+ prefix = "sensor",
+ idSuffix = "battery_level",
+ nameSuffix = "Battery Level",
+ deviceClass = "battery",
+ unit = "%",
+ valueTemplate = "{{ value | int }}",
+ binary = false
+ ),
+ SensorProvider.SensorConfiguration(
+ prefix = "sensor",
+ idSuffix = "battery_temperature",
+ nameSuffix = "Battery Temperature",
+ deviceClass = "temperature",
+ unit = "°C",
+ valueTemplate = "{{ value | float }}",
+ binary = false
+ ),
+ SensorProvider.SensorConfiguration(
+ prefix = "binary_sensor",
+ idSuffix = "charging",
+ nameSuffix = "Charging Status",
+ deviceClass = "battery_charging",
+ unit = null,
+ valueTemplate = null,
+ binary = true
+ )
+ )
+
+ override fun setDataListener(listener: SensorProvider.DataListener) {
+ dataListener = listener
+ Log.d(TAG, "Setting data listener, provider enabled: $isEnabled")
+
+ if (isEnabled) {
+ pendingBatteryLevel?.let {
+ dataListener?.onSensorData("battery_level", it.toString())
+ pendingBatteryLevel = null
+ }
+ pendingBatteryTemperature?.let {
+ dataListener?.onSensorData("battery_temperature", String.format("%.1f", it))
+ pendingBatteryTemperature = null
+ }
+ pendingChargingState?.let {
+ dataListener?.onSensorData("charging", if (it) "ON" else "OFF")
+ pendingChargingState = null
+ }
+
+ if (pendingBatteryLevel == null && pendingBatteryTemperature == null && pendingChargingState == null) {
+ forceUpdateBatteryStates()
+ }
+ } else {
+ notifyProvidersDisabled()
+ }
+ }
+
+ override fun onMqttConnected() {
+ Log.d(TAG, "MQTT connected, provider enabled: $isEnabled")
+ if (isEnabled) {
+ forceUpdateBatteryStates()
+ }
+ }
+
+ override fun stop() {
+ try {
+ unregisterReceiver(receiver)
+ } catch (e: IllegalArgumentException) {
+ Log.w(TAG, "Receiver not registered: ${e.message}")
+ }
+ sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/dev/tablaster/dashpanel/sensor/BatterySensorService.kt b/app/src/main/java/dev/tablaster/dashpanel/sensor/BatterySensorService.kt
deleted file mode 100644
index 03405ac..0000000
--- a/app/src/main/java/dev/tablaster/dashpanel/sensor/BatterySensorService.kt
+++ /dev/null
@@ -1,201 +0,0 @@
-package dev.tablaster.dashpanel.sensor
-
-import android.content.*
-import android.os.BatteryManager
-import android.os.Handler
-import android.os.Looper
-import android.util.Log
-import androidx.lifecycle.DefaultLifecycleObserver
-import androidx.lifecycle.LifecycleOwner
-import androidx.preference.PreferenceManager
-import dev.tablaster.dashpanel.mqtt.MQTTModule
-import dev.tablaster.dashpanel.mqtt.MQTTOptions
-import org.json.JSONObject
-
-class BatterySensorService(
- base: Context,
- private val mqttModule: MQTTModule,
- private val mqttOptions: MQTTOptions,
- private val updateInterval: Int
-) : ContextWrapper(base), DefaultLifecycleObserver {
-
- private var lastBatteryLevel = -1
- private var lastBatteryTemperature = -1f
- private var lastChargingState = false
-
- private var pendingBatteryLevel: Int? = null
- private var pendingBatteryTemperature: Float? = null
- private var pendingChargingState: Boolean? = null
-
- private val handler = Handler(Looper.getMainLooper())
- private val availabilityTopic = "${mqttOptions.getBaseTopic()}/dashpanel/connection"
- private val clientID = mqttOptions.getClientId()
- private val manufacturer = android.os.Build.MANUFACTURER ?: "Unknown"
- private val model = android.os.Build.MODEL ?: "Unknown"
- private val deviceId = android.os.Build.DEVICE ?: clientID
- private val baseTopic = mqttOptions.getBaseTopic()
-
- private val batteryReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- intent?.takeIf { it.action == Intent.ACTION_BATTERY_CHANGED }?.let {
- updateBatteryStates(it)
- }
- }
- }
-
- override fun onStart(owner: LifecycleOwner) {
- Log.d(TAG, "Registering battery receiver")
- registerReceiver(batteryReceiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
- publishAllDiscoveries()
- startPeriodicUpdates()
- }
-
- override fun onStop(owner: LifecycleOwner) {
- Log.d(TAG, "Unregistering battery receiver")
- stop()
- }
-
- private fun startPeriodicUpdates() {
- handler.postDelayed(object : Runnable {
- override fun run() {
- queryBatteryState()
- handler.postDelayed(this, updateInterval * 60 * 1000L)
- }
- }, updateInterval * 60 * 1000L)
- }
-
- private fun updateBatteryStates(intent: Intent, force: Boolean = false) {
- handleBatteryLevel(intent, force)
- handleBatteryTemperature(intent, force)
- handleChargingState(intent, force)
- }
-
- private fun handleBatteryLevel(intent: Intent, force: Boolean = false) {
- val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
- val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)
- if (level >= 0 && scale > 0) {
- val batteryPct = (level * 100) / scale
- if (force || batteryPct != lastBatteryLevel) {
- lastBatteryLevel = batteryPct
- publishOrCache("battery_level", batteryPct.toString()) { pendingBatteryLevel = batteryPct }
- }
- }
- }
-
- private fun handleBatteryTemperature(intent: Intent, force: Boolean = false) {
- val temperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1)
- if (temperature > 0) {
- val tempCelsius = temperature / 10.0f
- if (force || tempCelsius != lastBatteryTemperature) {
- lastBatteryTemperature = tempCelsius
- publishOrCache("battery_temperature", String.format("%.1f", tempCelsius)) {
- pendingBatteryTemperature = tempCelsius
- }
- }
- }
- }
-
- private fun handleChargingState(intent: Intent, force: Boolean = false) {
- val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
- val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
- if (force || isCharging != lastChargingState) {
- lastChargingState = isCharging
- publishOrCache("charging", if (isCharging) "ON" else "OFF") {
- pendingChargingState = isCharging
- }
- }
- }
-
- private fun publishOrCache(type: String, value: String, cache: () -> Unit) {
- if (mqttModule.isReady() == true) {
- publishState(type, value)
- } else {
- cache()
- }
- }
-
- private fun publishState(type: String, value: String) {
- val topic = when (type) {
- "battery_level" -> "$baseTopic/sensor/$model/battery_level/state"
- "battery_temperature" -> "$baseTopic/sensor/$model/battery_temperature/state"
- "charging" -> "$baseTopic/binary_sensor/$model/charging/state"
- else -> return
- }
- Log.d(TAG, "Publishing $type state: $value to $topic")
- mqttModule.publish(topic, value, retain = true)
- }
-
- private fun publishAllDiscoveries() {
- publishDiscovery("battery_level", "Battery Level", "battery_level", "battery", "%", "{{ value | int }}")
- publishDiscovery("battery_temperature", "Battery Temperature", "battery_temperature", "temperature", "°C", "{{ value | float }}")
- publishDiscovery("charging", "Charging Status", "charging", "battery_charging", null, null, binary = true)
- }
-
- private fun publishDiscovery(
- type: String,
- nameSuffix: String,
- idSuffix: String,
- deviceClass: String?,
- unit: String?,
- valueTemplate: String?,
- binary: Boolean = false
- ) {
- val configTopic = when (type) {
- "battery_level", "battery_temperature" -> "$baseTopic/sensor/$model/$idSuffix/config"
- "charging" -> "$baseTopic/binary_sensor/$model/$idSuffix/config"
- else -> return
- }
-
- val stateTopic = configTopic.replace("config", "state")
- val payload = JSONObject().apply {
- put("name", "$model $nameSuffix")
- put("unique_id", "${model}_$idSuffix")
- deviceClass?.let { put("device_class", it) }
- put("state_topic", stateTopic)
- put("availability_topic", availabilityTopic)
- unit?.let { put("unit_of_measurement", it) }
- valueTemplate?.let { put("value_template", it) }
- put("expire_after", updateInterval * 60 + 30)
- put("device", JSONObject().apply {
- put("identifiers", listOf(deviceId))
- put("name", model)
- put("manufacturer", manufacturer)
- put("model", model)
- put("sw_version", android.os.Build.VERSION.RELEASE)
- })
- if (binary) {
- put("payload_on", "ON")
- put("payload_off", "OFF")
- }
- }
-
- Log.d(TAG, "Publishing $type discovery to $configTopic")
- mqttModule.publish(configTopic, payload.toString(), retain = true)
- }
-
- private fun queryBatteryState() {
- registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))?.let {
- updateBatteryStates(it, force = true)
- }
- }
-
- fun onMqttConnected() {
- Log.d(TAG, "MQTT connected, publishing discovery and pending states")
- publishAllDiscoveries()
- queryBatteryState()
- }
-
- fun stop() {
- Log.d(TAG, "Stopping BatterySensorService")
- try {
- unregisterReceiver(batteryReceiver)
- } catch (e: IllegalArgumentException) {
- Log.w(TAG, "Receiver not registered: ${e.message}")
- }
- handler.removeCallbacksAndMessages(null)
- }
-
- companion object {
- private const val TAG = "BatterySensorService"
- }
-}
diff --git a/app/src/main/java/dev/tablaster/dashpanel/sensor/SensorProvider.kt b/app/src/main/java/dev/tablaster/dashpanel/sensor/SensorProvider.kt
new file mode 100644
index 0000000..892a97a
--- /dev/null
+++ b/app/src/main/java/dev/tablaster/dashpanel/sensor/SensorProvider.kt
@@ -0,0 +1,27 @@
+package dev.tablaster.dashpanel.sensor
+
+interface SensorProvider {
+ data class SensorConfiguration(
+ val prefix: String, // "sensor" or "binary_sensor"
+ val idSuffix: String, // e.g. "battery_level"
+ val nameSuffix: String, // e.g. "Battery Level"
+ val deviceClass: String?, // Device class for Home Assistant
+ val unit: String?, // Unit of measurement
+ val valueTemplate: String?,// Template for value parsing
+ val binary: Boolean // Binary sensor flag
+ )
+
+ fun getSensorConfigurations(): List<SensorConfiguration>
+
+ fun setDataListener(listener: DataListener)
+
+ fun updateSensorData()
+
+ fun onMqttConnected()
+
+ fun stop()
+
+ interface DataListener {
+ fun onSensorData(type: String, value: String)
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/dev/tablaster/dashpanel/utils/NotificationUtils.kt b/app/src/main/java/dev/tablaster/dashpanel/utils/NotificationUtils.kt
new file mode 100644
index 0000000..48baa85
--- /dev/null
+++ b/app/src/main/java/dev/tablaster/dashpanel/utils/NotificationUtils.kt
@@ -0,0 +1,75 @@
+package dev.tablaster.dashpanel.utils
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import dev.tablaster.dashpanel.MainActivity
+import dev.tablaster.dashpanel.R
+
+class NotificationUtils(private val context: Context) {
+
+ companion object {
+ private const val NOTIFICATION_ID = 1001
+ private const val CHANNEL_ID = "dashpanel_service_channel"
+ }
+
+ private val notificationManager: NotificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ init {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ createNotificationChannel()
+ }
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ "DashPanel Service",
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ description = "Service running in the background"
+ }
+ notificationManager.createNotificationChannel(channel)
+ }
+ }
+
+ private fun buildNotification(title: String, message: String): Notification {
+ val pendingIntent = PendingIntent.getActivity(
+ context, 0,
+ Intent(context, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE
+ )
+
+ return NotificationCompat.Builder(context, CHANNEL_ID)
+ .setContentTitle(title)
+ .setContentText(message)
+ .setSmallIcon(R.drawable.ic_logo)
+ .setContentIntent(pendingIntent)
+ .setOngoing(true)
+ .setAutoCancel(false)
+ .build()
+ }
+
+ fun startForegroundService() {
+ val notification = buildNotification(
+ context.getString(R.string.service_notification_title),
+ context.getString(R.string.service_notification_message)
+ )
+ notificationManager.notify(NOTIFICATION_ID, notification)
+ }
+
+ fun updateNotification(title: String, message: String) {
+ val notification = buildNotification(title, message)
+ notificationManager.notify(NOTIFICATION_ID, notification)
+ }
+
+ fun stopForegroundService() {
+ notificationManager.cancel(NOTIFICATION_ID)
+ }
+}