Loading app/src/main/java/io/heckel/ntfy/backup/Backuper.kt 0 → 100644 +314 −0 Original line number Diff line number Diff line package io.heckel.ntfy.backup import android.content.Context import android.net.Uri import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.stream.JsonReader import io.heckel.ntfy.app.Application import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.topicUrl import java.io.InputStreamReader class Backuper(val context: Context) { private val gson = Gson() private val resolver = context.applicationContext.contentResolver private val repository = (context.applicationContext as Application).repository suspend fun backup(uri: Uri, withSettings: Boolean = true, withSubscriptions: Boolean = true, withUsers: Boolean = true) { Log.d(TAG, "Backing up settings to file $uri") val json = gson.toJson(createBackupFile(withSettings, withSubscriptions, withUsers)) val outputStream = resolver.openOutputStream(uri) ?: throw Exception("Cannot open output stream") outputStream.use { it.write(json.toByteArray()) } Log.d(TAG, "Backup done") } suspend fun restore(uri: Uri) { Log.d(TAG, "Restoring settings from file $uri") val reader = JsonReader(InputStreamReader(resolver.openInputStream(uri))) val backupFile = gson.fromJson<BackupFile>(reader, BackupFile::class.java) applyBackupFile(backupFile) Log.d(TAG, "Restoring done") } fun settingsAsString(): String { val gson = GsonBuilder().setPrettyPrinting().create() return gson.toJson(createSettings()) } private suspend fun applyBackupFile(backupFile: BackupFile) { if (backupFile.magic != FILE_MAGIC) { throw InvalidBackupFileException() } applySettings(backupFile.settings) applySubscriptions(backupFile.subscriptions) applyNotifications(backupFile.notifications) applyUsers(backupFile.users) } private fun applySettings(settings: Settings?) { if (settings == null) { return } if (settings.minPriority != null) { repository.setMinPriority(settings.minPriority) } if (settings.autoDownloadMaxSize != null) { repository.setAutoDownloadMaxSize(settings.autoDownloadMaxSize) } if (settings.autoDeleteSeconds != null) { repository.setAutoDeleteSeconds(settings.autoDeleteSeconds) } if (settings.darkMode != null) { repository.setDarkMode(settings.darkMode) } if (settings.connectionProtocol != null) { repository.setConnectionProtocol(settings.connectionProtocol) } if (settings.broadcastEnabled != null) { repository.setBroadcastEnabled(settings.broadcastEnabled) } if (settings.recordLogs != null) { repository.setRecordLogsEnabled(settings.recordLogs) } if (settings.defaultBaseUrl != null) { repository.setDefaultBaseUrl(settings.defaultBaseUrl) } if (settings.mutedUntil != null) { repository.setGlobalMutedUntil(settings.mutedUntil) } if (settings.lastSharedTopics != null) { settings.lastSharedTopics.forEach { repository.addLastShareTopic(it) } } } private suspend fun applySubscriptions(subscriptions: List<Subscription>?) { if (subscriptions == null) { return; } subscriptions.forEach { s -> try { repository.addSubscription(io.heckel.ntfy.db.Subscription( id = s.id, baseUrl = s.baseUrl, topic = s.topic, instant = s.instant, mutedUntil = s.mutedUntil, upAppId = s.upAppId, upConnectorToken = s.upConnectorToken )) } catch (e: Exception) { Log.w(TAG, "Unable to restore subscription ${s.id} (${topicUrl(s.baseUrl, s.topic)}): ${e.message}. Ignoring.", e) } } } private suspend fun applyNotifications(notifications: List<Notification>?) { if (notifications == null) { return; } notifications.forEach { n -> try { val attachment = if (n.attachment != null) { io.heckel.ntfy.db.Attachment( name = n.attachment.name, type = n.attachment.type, size = n.attachment.size, expires = n.attachment.expires, url = n.attachment.url, contentUri = n.attachment.contentUri, progress = n.attachment.progress, ) } else { null } repository.addNotification(io.heckel.ntfy.db.Notification( id = n.id, subscriptionId = n.subscriptionId, timestamp = n.timestamp, title = n.title, message = n.message, encoding = n.encoding, notificationId = 0, priority = n.priority, tags = n.tags, click = n.click, attachment = attachment, deleted = n.deleted )) } catch (e: Exception) { Log.w(TAG, "Unable to restore notification ${n.id}: ${e.message}. Ignoring.", e) } } } private suspend fun applyUsers(users: List<User>?) { if (users == null) { return; } users.forEach { u -> try { repository.addUser(io.heckel.ntfy.db.User( baseUrl = u.baseUrl, username = u.username, password = u.password )) } catch (e: Exception) { Log.w(TAG, "Unable to restore user ${u.baseUrl} / ${u.username}: ${e.message}. Ignoring.", e) } } } private suspend fun createBackupFile(withSettings: Boolean, withSubscriptions: Boolean, withUsers: Boolean): BackupFile { return BackupFile( magic = FILE_MAGIC, version = FILE_VERSION, settings = if (withSettings) createSettings() else null, subscriptions = if (withSubscriptions) createSubscriptionList() else null, notifications = if (withSubscriptions) createNotificationList() else null, users = if (withUsers) createUserList() else null ) } private fun createSettings(): Settings { return Settings( minPriority = repository.getMinPriority(), autoDownloadMaxSize = repository.getAutoDownloadMaxSize(), autoDeleteSeconds = repository.getAutoDeleteSeconds(), darkMode = repository.getDarkMode(), connectionProtocol = repository.getConnectionProtocol(), broadcastEnabled = repository.getBroadcastEnabled(), recordLogs = repository.getRecordLogs(), defaultBaseUrl = repository.getDefaultBaseUrl() ?: "", mutedUntil = repository.getGlobalMutedUntil(), lastSharedTopics = repository.getLastShareTopics() ) } private suspend fun createSubscriptionList(): List<Subscription> { return repository.getSubscriptions().map { s -> Subscription( id = s.id, baseUrl = s.baseUrl, topic = s.topic, instant = s.instant, mutedUntil = s.mutedUntil, upAppId = s.upAppId, upConnectorToken = s.upConnectorToken ) } } private suspend fun createNotificationList(): List<Notification> { return repository.getNotifications().map { n -> val attachment = if (n.attachment != null) { Attachment( name = n.attachment.name, type = n.attachment.type, size = n.attachment.size, expires = n.attachment.expires, url = n.attachment.url, contentUri = n.attachment.contentUri, progress = n.attachment.progress, ) } else { null } Notification( id = n.id, subscriptionId = n.subscriptionId, timestamp = n.timestamp, title = n.title, message = n.message, encoding = n.encoding, priority = n.priority, tags = n.tags, click = n.click, attachment = attachment, deleted = n.deleted ) } } private suspend fun createUserList(): List<User> { return repository.getUsers().map { u -> User( baseUrl = u.baseUrl, username = u.username, password = u.password ) } } companion object { const val MIME_TYPE = "application/json" private const val FILE_MAGIC = "ntfy2586" private const val FILE_VERSION = 1 private const val TAG = "NtfyExporter" } } data class BackupFile( val magic: String, val version: Int, val settings: Settings?, val subscriptions: List<Subscription>?, val notifications: List<Notification>?, val users: List<User>? ) data class Settings( val minPriority: Int?, val autoDownloadMaxSize: Long?, val autoDeleteSeconds: Long?, val darkMode: Int?, val connectionProtocol: String?, val broadcastEnabled: Boolean?, val recordLogs: Boolean?, val defaultBaseUrl: String?, val mutedUntil: Long?, val lastSharedTopics: List<String>?, ) data class Subscription( val id: Long, val baseUrl: String, val topic: String, val instant: Boolean, val mutedUntil: Long, val upAppId: String?, val upConnectorToken: String? ) data class Notification( val id: String, val subscriptionId: Long, val timestamp: Long, val title: String, val message: String, val encoding: String, // "base64" or "" val priority: Int, // 1=min, 3=default, 5=max val tags: String, val click: String, // URL/intent to open on notification click val attachment: Attachment?, val deleted: Boolean ) data class Attachment( val name: String, // Filename val type: String?, // MIME type val size: Long?, // Size in bytes val expires: Long?, // Unix timestamp val url: String, // URL (mandatory, see ntfy server) val contentUri: String?, // After it's downloaded, the content:// location val progress: Int, // Progress during download, -1 if not downloaded ) data class User( val baseUrl: String, val username: String, val password: String ) class InvalidBackupFileException : Exception("Invalid backup file format") app/src/main/java/io/heckel/ntfy/db/Database.kt +7 −1 Original line number Diff line number Diff line Loading @@ -228,7 +228,7 @@ interface SubscriptionDao { GROUP BY s.id ORDER BY s.upAppId ASC, MAX(n.timestamp) DESC """) fun list(): List<SubscriptionWithMetadata> suspend fun list(): List<SubscriptionWithMetadata> @Query(""" SELECT Loading Loading @@ -281,6 +281,9 @@ interface SubscriptionDao { @Dao interface NotificationDao { @Query("SELECT * FROM notification") suspend fun list(): List<Notification> @Query("SELECT * FROM notification WHERE subscriptionId = :subscriptionId AND deleted != 1 ORDER BY timestamp DESC") fun listFlow(subscriptionId: Long): Flow<List<Notification>> Loading Loading @@ -326,6 +329,9 @@ interface UserDao { @Query("SELECT * FROM user ORDER BY username") suspend fun list(): List<User> @Query("SELECT * FROM user ORDER BY username") fun listFlow(): Flow<List<User>> @Query("SELECT * FROM user WHERE baseUrl = :baseUrl") suspend fun get(baseUrl: String): User? Loading app/src/main/java/io/heckel/ntfy/db/Repository.kt +10 −2 Original line number Diff line number Diff line Loading @@ -40,11 +40,11 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas .map { list -> list.map { Pair(it.id, it.instant) }.toSet() } } fun getSubscriptions(): List<Subscription> { suspend fun getSubscriptions(): List<Subscription> { return toSubscriptionList(subscriptionDao.list()) } fun getSubscriptionIdsWithInstantStatus(): Set<Pair<Long, Boolean>> { suspend fun getSubscriptionIdsWithInstantStatus(): Set<Pair<Long, Boolean>> { return subscriptionDao .list() .map { Pair(it.id, it.instant) }.toSet() Loading Loading @@ -84,6 +84,10 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas subscriptionDao.remove(subscriptionId) } suspend fun getNotifications(): List<Notification> { return notificationDao.list() } fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> { return notificationDao.listFlow(subscriptionId).asLiveData() } Loading Loading @@ -144,6 +148,10 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas return userDao.list() } fun getUsersLiveData(): LiveData<List<User>> { return userDao.listFlow().asLiveData() } suspend fun addUser(user: User) { userDao.insert(user) } Loading app/src/main/java/io/heckel/ntfy/msg/DownloadWorker.kt +1 −19 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ import io.heckel.ntfy.R import io.heckel.ntfy.app.Application import io.heckel.ntfy.db.* import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.ensureSafeNewFile import io.heckel.ntfy.util.fileName import okhttp3.OkHttpClient import okhttp3.Request Loading Loading @@ -216,25 +217,6 @@ class DownloadWorker(private val context: Context, params: WorkerParameters) : W } } private fun ensureSafeNewFile(dir: File, name: String): File { val safeName = name.replace("[^-_.()\\w]+".toRegex(), "_"); val file = File(dir, safeName) if (!file.exists()) { return file } (1..1000).forEach { i -> val newFile = File(dir, if (file.extension == "") { "${file.nameWithoutExtension} ($i)" } else { "${file.nameWithoutExtension} ($i).${file.extension}" }) if (!newFile.exists()) { return newFile } } throw Exception("Cannot find safe file") } companion object { const val INPUT_DATA_ID = "id" const val INPUT_DATA_USER_ACTION = "userAction" Loading app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt +20 −15 Original line number Diff line number Diff line Loading @@ -6,6 +6,8 @@ import androidx.core.content.ContextCompat import androidx.work.* import io.heckel.ntfy.app.Application import io.heckel.ntfy.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext /** * This class only manages the SubscriberService, i.e. it starts or stops it. Loading Loading @@ -34,25 +36,28 @@ class SubscriberServiceManager(private val context: Context) { * Starts or stops the foreground service by figuring out how many instant delivery subscriptions * exist. If there's > 0, then we need a foreground service. */ class ServiceStartWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) { override fun doWork(): Result { class ServiceStartWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val id = this.id if (context.applicationContext !is Application) { Log.d(TAG, "ServiceStartWorker: Failed, no application found (work ID: ${this.id})") Log.d(TAG, "ServiceStartWorker: Failed, no application found (work ID: ${id})") return Result.failure() } withContext(Dispatchers.IO) { val app = context.applicationContext as Application val subscriptionIdsWithInstantStatus = app.repository.getSubscriptionIdsWithInstantStatus() val instantSubscriptions = subscriptionIdsWithInstantStatus.toList().filter { (_, instant) -> instant }.size val action = if (instantSubscriptions > 0) SubscriberService.Action.START else SubscriberService.Action.STOP val serviceState = SubscriberService.readServiceState(context) if (serviceState == SubscriberService.ServiceState.STOPPED && action == SubscriberService.Action.STOP) { return Result.success() return@withContext Result.success() } Log.d(TAG, "ServiceStartWorker: Starting foreground service with action $action (work ID: ${this.id})") Log.d(TAG, "ServiceStartWorker: Starting foreground service with action $action (work ID: ${id})") Intent(context, SubscriberService::class.java).also { it.action = action.name ContextCompat.startForegroundService(context, it) } } return Result.success() } } Loading Loading
app/src/main/java/io/heckel/ntfy/backup/Backuper.kt 0 → 100644 +314 −0 Original line number Diff line number Diff line package io.heckel.ntfy.backup import android.content.Context import android.net.Uri import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.stream.JsonReader import io.heckel.ntfy.app.Application import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.topicUrl import java.io.InputStreamReader class Backuper(val context: Context) { private val gson = Gson() private val resolver = context.applicationContext.contentResolver private val repository = (context.applicationContext as Application).repository suspend fun backup(uri: Uri, withSettings: Boolean = true, withSubscriptions: Boolean = true, withUsers: Boolean = true) { Log.d(TAG, "Backing up settings to file $uri") val json = gson.toJson(createBackupFile(withSettings, withSubscriptions, withUsers)) val outputStream = resolver.openOutputStream(uri) ?: throw Exception("Cannot open output stream") outputStream.use { it.write(json.toByteArray()) } Log.d(TAG, "Backup done") } suspend fun restore(uri: Uri) { Log.d(TAG, "Restoring settings from file $uri") val reader = JsonReader(InputStreamReader(resolver.openInputStream(uri))) val backupFile = gson.fromJson<BackupFile>(reader, BackupFile::class.java) applyBackupFile(backupFile) Log.d(TAG, "Restoring done") } fun settingsAsString(): String { val gson = GsonBuilder().setPrettyPrinting().create() return gson.toJson(createSettings()) } private suspend fun applyBackupFile(backupFile: BackupFile) { if (backupFile.magic != FILE_MAGIC) { throw InvalidBackupFileException() } applySettings(backupFile.settings) applySubscriptions(backupFile.subscriptions) applyNotifications(backupFile.notifications) applyUsers(backupFile.users) } private fun applySettings(settings: Settings?) { if (settings == null) { return } if (settings.minPriority != null) { repository.setMinPriority(settings.minPriority) } if (settings.autoDownloadMaxSize != null) { repository.setAutoDownloadMaxSize(settings.autoDownloadMaxSize) } if (settings.autoDeleteSeconds != null) { repository.setAutoDeleteSeconds(settings.autoDeleteSeconds) } if (settings.darkMode != null) { repository.setDarkMode(settings.darkMode) } if (settings.connectionProtocol != null) { repository.setConnectionProtocol(settings.connectionProtocol) } if (settings.broadcastEnabled != null) { repository.setBroadcastEnabled(settings.broadcastEnabled) } if (settings.recordLogs != null) { repository.setRecordLogsEnabled(settings.recordLogs) } if (settings.defaultBaseUrl != null) { repository.setDefaultBaseUrl(settings.defaultBaseUrl) } if (settings.mutedUntil != null) { repository.setGlobalMutedUntil(settings.mutedUntil) } if (settings.lastSharedTopics != null) { settings.lastSharedTopics.forEach { repository.addLastShareTopic(it) } } } private suspend fun applySubscriptions(subscriptions: List<Subscription>?) { if (subscriptions == null) { return; } subscriptions.forEach { s -> try { repository.addSubscription(io.heckel.ntfy.db.Subscription( id = s.id, baseUrl = s.baseUrl, topic = s.topic, instant = s.instant, mutedUntil = s.mutedUntil, upAppId = s.upAppId, upConnectorToken = s.upConnectorToken )) } catch (e: Exception) { Log.w(TAG, "Unable to restore subscription ${s.id} (${topicUrl(s.baseUrl, s.topic)}): ${e.message}. Ignoring.", e) } } } private suspend fun applyNotifications(notifications: List<Notification>?) { if (notifications == null) { return; } notifications.forEach { n -> try { val attachment = if (n.attachment != null) { io.heckel.ntfy.db.Attachment( name = n.attachment.name, type = n.attachment.type, size = n.attachment.size, expires = n.attachment.expires, url = n.attachment.url, contentUri = n.attachment.contentUri, progress = n.attachment.progress, ) } else { null } repository.addNotification(io.heckel.ntfy.db.Notification( id = n.id, subscriptionId = n.subscriptionId, timestamp = n.timestamp, title = n.title, message = n.message, encoding = n.encoding, notificationId = 0, priority = n.priority, tags = n.tags, click = n.click, attachment = attachment, deleted = n.deleted )) } catch (e: Exception) { Log.w(TAG, "Unable to restore notification ${n.id}: ${e.message}. Ignoring.", e) } } } private suspend fun applyUsers(users: List<User>?) { if (users == null) { return; } users.forEach { u -> try { repository.addUser(io.heckel.ntfy.db.User( baseUrl = u.baseUrl, username = u.username, password = u.password )) } catch (e: Exception) { Log.w(TAG, "Unable to restore user ${u.baseUrl} / ${u.username}: ${e.message}. Ignoring.", e) } } } private suspend fun createBackupFile(withSettings: Boolean, withSubscriptions: Boolean, withUsers: Boolean): BackupFile { return BackupFile( magic = FILE_MAGIC, version = FILE_VERSION, settings = if (withSettings) createSettings() else null, subscriptions = if (withSubscriptions) createSubscriptionList() else null, notifications = if (withSubscriptions) createNotificationList() else null, users = if (withUsers) createUserList() else null ) } private fun createSettings(): Settings { return Settings( minPriority = repository.getMinPriority(), autoDownloadMaxSize = repository.getAutoDownloadMaxSize(), autoDeleteSeconds = repository.getAutoDeleteSeconds(), darkMode = repository.getDarkMode(), connectionProtocol = repository.getConnectionProtocol(), broadcastEnabled = repository.getBroadcastEnabled(), recordLogs = repository.getRecordLogs(), defaultBaseUrl = repository.getDefaultBaseUrl() ?: "", mutedUntil = repository.getGlobalMutedUntil(), lastSharedTopics = repository.getLastShareTopics() ) } private suspend fun createSubscriptionList(): List<Subscription> { return repository.getSubscriptions().map { s -> Subscription( id = s.id, baseUrl = s.baseUrl, topic = s.topic, instant = s.instant, mutedUntil = s.mutedUntil, upAppId = s.upAppId, upConnectorToken = s.upConnectorToken ) } } private suspend fun createNotificationList(): List<Notification> { return repository.getNotifications().map { n -> val attachment = if (n.attachment != null) { Attachment( name = n.attachment.name, type = n.attachment.type, size = n.attachment.size, expires = n.attachment.expires, url = n.attachment.url, contentUri = n.attachment.contentUri, progress = n.attachment.progress, ) } else { null } Notification( id = n.id, subscriptionId = n.subscriptionId, timestamp = n.timestamp, title = n.title, message = n.message, encoding = n.encoding, priority = n.priority, tags = n.tags, click = n.click, attachment = attachment, deleted = n.deleted ) } } private suspend fun createUserList(): List<User> { return repository.getUsers().map { u -> User( baseUrl = u.baseUrl, username = u.username, password = u.password ) } } companion object { const val MIME_TYPE = "application/json" private const val FILE_MAGIC = "ntfy2586" private const val FILE_VERSION = 1 private const val TAG = "NtfyExporter" } } data class BackupFile( val magic: String, val version: Int, val settings: Settings?, val subscriptions: List<Subscription>?, val notifications: List<Notification>?, val users: List<User>? ) data class Settings( val minPriority: Int?, val autoDownloadMaxSize: Long?, val autoDeleteSeconds: Long?, val darkMode: Int?, val connectionProtocol: String?, val broadcastEnabled: Boolean?, val recordLogs: Boolean?, val defaultBaseUrl: String?, val mutedUntil: Long?, val lastSharedTopics: List<String>?, ) data class Subscription( val id: Long, val baseUrl: String, val topic: String, val instant: Boolean, val mutedUntil: Long, val upAppId: String?, val upConnectorToken: String? ) data class Notification( val id: String, val subscriptionId: Long, val timestamp: Long, val title: String, val message: String, val encoding: String, // "base64" or "" val priority: Int, // 1=min, 3=default, 5=max val tags: String, val click: String, // URL/intent to open on notification click val attachment: Attachment?, val deleted: Boolean ) data class Attachment( val name: String, // Filename val type: String?, // MIME type val size: Long?, // Size in bytes val expires: Long?, // Unix timestamp val url: String, // URL (mandatory, see ntfy server) val contentUri: String?, // After it's downloaded, the content:// location val progress: Int, // Progress during download, -1 if not downloaded ) data class User( val baseUrl: String, val username: String, val password: String ) class InvalidBackupFileException : Exception("Invalid backup file format")
app/src/main/java/io/heckel/ntfy/db/Database.kt +7 −1 Original line number Diff line number Diff line Loading @@ -228,7 +228,7 @@ interface SubscriptionDao { GROUP BY s.id ORDER BY s.upAppId ASC, MAX(n.timestamp) DESC """) fun list(): List<SubscriptionWithMetadata> suspend fun list(): List<SubscriptionWithMetadata> @Query(""" SELECT Loading Loading @@ -281,6 +281,9 @@ interface SubscriptionDao { @Dao interface NotificationDao { @Query("SELECT * FROM notification") suspend fun list(): List<Notification> @Query("SELECT * FROM notification WHERE subscriptionId = :subscriptionId AND deleted != 1 ORDER BY timestamp DESC") fun listFlow(subscriptionId: Long): Flow<List<Notification>> Loading Loading @@ -326,6 +329,9 @@ interface UserDao { @Query("SELECT * FROM user ORDER BY username") suspend fun list(): List<User> @Query("SELECT * FROM user ORDER BY username") fun listFlow(): Flow<List<User>> @Query("SELECT * FROM user WHERE baseUrl = :baseUrl") suspend fun get(baseUrl: String): User? Loading
app/src/main/java/io/heckel/ntfy/db/Repository.kt +10 −2 Original line number Diff line number Diff line Loading @@ -40,11 +40,11 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas .map { list -> list.map { Pair(it.id, it.instant) }.toSet() } } fun getSubscriptions(): List<Subscription> { suspend fun getSubscriptions(): List<Subscription> { return toSubscriptionList(subscriptionDao.list()) } fun getSubscriptionIdsWithInstantStatus(): Set<Pair<Long, Boolean>> { suspend fun getSubscriptionIdsWithInstantStatus(): Set<Pair<Long, Boolean>> { return subscriptionDao .list() .map { Pair(it.id, it.instant) }.toSet() Loading Loading @@ -84,6 +84,10 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas subscriptionDao.remove(subscriptionId) } suspend fun getNotifications(): List<Notification> { return notificationDao.list() } fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> { return notificationDao.listFlow(subscriptionId).asLiveData() } Loading Loading @@ -144,6 +148,10 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas return userDao.list() } fun getUsersLiveData(): LiveData<List<User>> { return userDao.listFlow().asLiveData() } suspend fun addUser(user: User) { userDao.insert(user) } Loading
app/src/main/java/io/heckel/ntfy/msg/DownloadWorker.kt +1 −19 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ import io.heckel.ntfy.R import io.heckel.ntfy.app.Application import io.heckel.ntfy.db.* import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.ensureSafeNewFile import io.heckel.ntfy.util.fileName import okhttp3.OkHttpClient import okhttp3.Request Loading Loading @@ -216,25 +217,6 @@ class DownloadWorker(private val context: Context, params: WorkerParameters) : W } } private fun ensureSafeNewFile(dir: File, name: String): File { val safeName = name.replace("[^-_.()\\w]+".toRegex(), "_"); val file = File(dir, safeName) if (!file.exists()) { return file } (1..1000).forEach { i -> val newFile = File(dir, if (file.extension == "") { "${file.nameWithoutExtension} ($i)" } else { "${file.nameWithoutExtension} ($i).${file.extension}" }) if (!newFile.exists()) { return newFile } } throw Exception("Cannot find safe file") } companion object { const val INPUT_DATA_ID = "id" const val INPUT_DATA_USER_ACTION = "userAction" Loading
app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt +20 −15 Original line number Diff line number Diff line Loading @@ -6,6 +6,8 @@ import androidx.core.content.ContextCompat import androidx.work.* import io.heckel.ntfy.app.Application import io.heckel.ntfy.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext /** * This class only manages the SubscriberService, i.e. it starts or stops it. Loading Loading @@ -34,25 +36,28 @@ class SubscriberServiceManager(private val context: Context) { * Starts or stops the foreground service by figuring out how many instant delivery subscriptions * exist. If there's > 0, then we need a foreground service. */ class ServiceStartWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) { override fun doWork(): Result { class ServiceStartWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val id = this.id if (context.applicationContext !is Application) { Log.d(TAG, "ServiceStartWorker: Failed, no application found (work ID: ${this.id})") Log.d(TAG, "ServiceStartWorker: Failed, no application found (work ID: ${id})") return Result.failure() } withContext(Dispatchers.IO) { val app = context.applicationContext as Application val subscriptionIdsWithInstantStatus = app.repository.getSubscriptionIdsWithInstantStatus() val instantSubscriptions = subscriptionIdsWithInstantStatus.toList().filter { (_, instant) -> instant }.size val action = if (instantSubscriptions > 0) SubscriberService.Action.START else SubscriberService.Action.STOP val serviceState = SubscriberService.readServiceState(context) if (serviceState == SubscriberService.ServiceState.STOPPED && action == SubscriberService.Action.STOP) { return Result.success() return@withContext Result.success() } Log.d(TAG, "ServiceStartWorker: Starting foreground service with action $action (work ID: ${this.id})") Log.d(TAG, "ServiceStartWorker: Starting foreground service with action $action (work ID: ${id})") Intent(context, SubscriberService::class.java).also { it.action = action.name ContextCompat.startForegroundService(context, it) } } return Result.success() } } Loading