- Auto download toggle
- Do not update notification if not visible - Detail view menu
This commit is contained in:
parent
95e101eb65
commit
e88f87390e
19 changed files with 273 additions and 68 deletions
|
@ -68,9 +68,13 @@ data class Attachment(
|
|||
@ColumnInfo(name = "progress") val progress: Int,
|
||||
) {
|
||||
constructor(name: String?, type: String?, size: Long?, expires: Long?, url: String) :
|
||||
this(name, type, size, expires, url, null, 0)
|
||||
this(name, type, size, expires, url, null, PROGRESS_NONE)
|
||||
}
|
||||
|
||||
const val PROGRESS_NONE = -1
|
||||
const val PROGRESS_INDETERMINATE = -2
|
||||
const val PROGRESS_DONE = 100
|
||||
|
||||
@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 6)
|
||||
abstract class Database : RoomDatabase() {
|
||||
abstract fun subscriptionDao(): SubscriptionDao
|
||||
|
|
|
@ -161,6 +161,16 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
|
|||
return sharedPrefs.getInt(SHARED_PREFS_MIN_PRIORITY, 1) // 1/low means all priorities
|
||||
}
|
||||
|
||||
fun getAutoDownloadEnabled(): Boolean {
|
||||
return sharedPrefs.getBoolean(SHARED_PREFS_AUTO_DOWNLOAD_ENABLED, true) // Enabled by default
|
||||
}
|
||||
|
||||
fun setAutoDownloadEnabled(enabled: Boolean) {
|
||||
sharedPrefs.edit()
|
||||
.putBoolean(SHARED_PREFS_AUTO_DOWNLOAD_ENABLED, enabled)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun getBroadcastEnabled(): Boolean {
|
||||
return sharedPrefs.getBoolean(SHARED_PREFS_BROADCAST_ENABLED, true) // Enabled by default
|
||||
}
|
||||
|
@ -291,6 +301,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
|
|||
const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion"
|
||||
const val SHARED_PREFS_MUTED_UNTIL_TIMESTAMP = "MutedUntil"
|
||||
const val SHARED_PREFS_MIN_PRIORITY = "MinPriority"
|
||||
const val SHARED_PREFS_AUTO_DOWNLOAD_ENABLED = "AutoDownload"
|
||||
const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled"
|
||||
const val SHARED_PREFS_UNIFIED_PUSH_ENABLED = "UnifiedPushEnabled"
|
||||
const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL"
|
||||
|
|
|
@ -8,11 +8,7 @@ import android.util.Log
|
|||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import io.heckel.ntfy.app.Application
|
||||
import io.heckel.ntfy.data.Attachment
|
||||
import io.heckel.ntfy.data.Notification
|
||||
import io.heckel.ntfy.data.Repository
|
||||
import io.heckel.ntfy.data.Subscription
|
||||
import io.heckel.ntfy.msg.NotificationService.Companion.PROGRESS_DONE
|
||||
import io.heckel.ntfy.data.*
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -33,12 +29,12 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam
|
|||
val repository = app.repository
|
||||
val notification = repository.getNotification(notificationId) ?: return Result.failure()
|
||||
val subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure()
|
||||
val attachment = notification.attachment ?: return Result.failure()
|
||||
downloadAttachment(repository, subscription, notification, attachment)
|
||||
downloadAttachment(repository, subscription, notification)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun downloadAttachment(repository: Repository, subscription: Subscription, notification: Notification, attachment: Attachment) {
|
||||
private fun downloadAttachment(repository: Repository, subscription: Subscription, notification: Notification) {
|
||||
val attachment = notification.attachment ?: return
|
||||
Log.d(TAG, "Downloading attachment from ${attachment.url}")
|
||||
|
||||
val request = Request.Builder()
|
||||
|
@ -71,8 +67,11 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam
|
|||
var lastProgress = 0L
|
||||
while (bytes >= 0) {
|
||||
if (System.currentTimeMillis() - lastProgress > 500) {
|
||||
val progress = if (size > 0) (bytesCopied.toFloat()/size.toFloat()*100).toInt() else NotificationService.PROGRESS_INDETERMINATE
|
||||
notifier.update(subscription, notification, progress = progress)
|
||||
val progress = if (size > 0) (bytesCopied.toFloat()/size.toFloat()*100).toInt() else PROGRESS_INDETERMINATE
|
||||
val newAttachment = attachment.copy(progress = progress)
|
||||
val newNotification = notification.copy(attachment = newAttachment)
|
||||
notifier.update(subscription, newNotification)
|
||||
repository.updateNotification(newNotification)
|
||||
lastProgress = System.currentTimeMillis()
|
||||
}
|
||||
fileOut.write(buffer, 0, bytes)
|
||||
|
@ -81,10 +80,10 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam
|
|||
}
|
||||
}
|
||||
Log.d(TAG, "Attachment download: successful response, proceeding with download")
|
||||
val newAttachment = attachment.copy(contentUri = uri.toString())
|
||||
val newAttachment = attachment.copy(contentUri = uri.toString(), progress = PROGRESS_DONE)
|
||||
val newNotification = notification.copy(attachment = newAttachment)
|
||||
repository.updateNotification(newNotification)
|
||||
notifier.update(subscription, newNotification, progress = PROGRESS_DONE)
|
||||
notifier.update(subscription, newNotification)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
|
|||
}
|
||||
|
||||
private fun shouldDownload(subscription: Subscription, notification: Notification): Boolean {
|
||||
return notification.attachment != null
|
||||
return notification.attachment != null && repository.getAutoDownloadEnabled()
|
||||
}
|
||||
|
||||
private fun shouldNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean {
|
||||
|
@ -84,12 +84,10 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
|
|||
}
|
||||
|
||||
private fun scheduleAttachmentDownload(notification: Notification) {
|
||||
Log.d(TAG, "Enqueuing work to download attachment (+ preview if available)")
|
||||
Log.d(TAG, "Enqueuing work to download attachment")
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
val workRequest = OneTimeWorkRequest.Builder(AttachmentDownloadWorker::class.java)
|
||||
.setInputData(workDataOf(
|
||||
"id" to notification.id,
|
||||
))
|
||||
.setInputData(workDataOf("id" to notification.id))
|
||||
.build()
|
||||
workManager.enqueue(workRequest)
|
||||
}
|
||||
|
|
|
@ -24,15 +24,21 @@ class NotificationService(val context: Context) {
|
|||
displayInternal(subscription, notification)
|
||||
}
|
||||
|
||||
fun update(subscription: Subscription, notification: Notification, progress: Int = PROGRESS_NONE) {
|
||||
fun update(subscription: Subscription, notification: Notification) {
|
||||
val active = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
notificationManager.activeNotifications.find { it.id == notification.notificationId } != null
|
||||
} else {
|
||||
true
|
||||
}
|
||||
if (active) {
|
||||
Log.d(TAG, "Updating notification $notification")
|
||||
displayInternal(subscription, notification, update = true, progress = progress)
|
||||
displayInternal(subscription, notification, update = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel(notification: Notification) {
|
||||
if (notification.notificationId != 0) {
|
||||
Log.d(TAG, "Cancelling notification ${notification.id}: ${notification.message}")
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancel(notification.notificationId)
|
||||
}
|
||||
}
|
||||
|
@ -41,9 +47,9 @@ class NotificationService(val context: Context) {
|
|||
(1..5).forEach { priority -> maybeCreateNotificationChannel(priority) }
|
||||
}
|
||||
|
||||
private fun displayInternal(subscription: Subscription, notification: Notification, update: Boolean = false, progress: Int = PROGRESS_NONE) {
|
||||
private fun displayInternal(subscription: Subscription, notification: Notification, update: Boolean = false) {
|
||||
val title = formatTitle(subscription, notification)
|
||||
val message = maybeWithAttachmentInfo(formatMessage(notification), notification, progress)
|
||||
val message = maybeWithAttachmentInfo(formatMessage(notification), notification)
|
||||
val channelId = toChannelId(notification.priority)
|
||||
val builder = NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
|
@ -55,7 +61,7 @@ class NotificationService(val context: Context) {
|
|||
setStyle(builder, notification, message) // Preview picture or big text style
|
||||
setContentIntent(builder, subscription, notification)
|
||||
maybeSetSound(builder, update)
|
||||
maybeSetProgress(builder, progress)
|
||||
maybeSetProgress(builder, notification)
|
||||
maybeAddOpenAction(builder, notification)
|
||||
maybeAddBrowseAction(builder, notification)
|
||||
|
||||
|
@ -63,16 +69,17 @@ class NotificationService(val context: Context) {
|
|||
notificationManager.notify(notification.notificationId, builder.build())
|
||||
}
|
||||
|
||||
private fun maybeWithAttachmentInfo(message: String, notification: Notification, progress: Int): String {
|
||||
if (progress < 0 || notification.attachment == null) return message
|
||||
val att = notification.attachment
|
||||
// FIXME duplicate code
|
||||
private fun maybeWithAttachmentInfo(message: String, notification: Notification): String {
|
||||
val att = notification.attachment ?: return message
|
||||
if (att.progress < 0) return message
|
||||
val infos = mutableListOf<String>()
|
||||
if (att.name != null) infos.add(att.name)
|
||||
if (att.size != null) infos.add(formatBytes(att.size))
|
||||
//if (att.expires != null && att.expires != 0L) infos.add(formatDateShort(att.expires))
|
||||
if (progress in 0..99) infos.add("${progress}%")
|
||||
if (att.progress in 0..99) infos.add("${att.progress}%")
|
||||
if (infos.size == 0) return message
|
||||
if (progress < 100) return "Downloading ${infos.joinToString(", ")}\n${message}"
|
||||
if (att.progress < 100) return "Downloading ${infos.joinToString(", ")}\n${message}"
|
||||
return "${message}\nFile: ${infos.joinToString(", ")}"
|
||||
}
|
||||
|
||||
|
@ -120,9 +127,10 @@ class NotificationService(val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun maybeSetProgress(builder: NotificationCompat.Builder, progress: Int) {
|
||||
private fun maybeSetProgress(builder: NotificationCompat.Builder, notification: Notification) {
|
||||
val progress = notification.attachment?.progress
|
||||
if (progress in 0..99) {
|
||||
builder.setProgress(100, progress, false)
|
||||
builder.setProgress(100, progress!!, false)
|
||||
} else {
|
||||
builder.setProgress(0, 0, false) // Remove progress bar
|
||||
}
|
||||
|
@ -206,10 +214,6 @@ class NotificationService(val context: Context) {
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val PROGRESS_NONE = -1
|
||||
const val PROGRESS_INDETERMINATE = -2
|
||||
const val PROGRESS_DONE = 100
|
||||
|
||||
private const val TAG = "NtfyNotifService"
|
||||
|
||||
private const val CHANNEL_ID_MIN = "ntfy-min"
|
||||
|
|
|
@ -87,8 +87,8 @@ class SubscriberConnection(
|
|||
|
||||
fun cancel() {
|
||||
Log.d(TAG, "[$url] Cancelling connection")
|
||||
job?.cancel()
|
||||
call?.cancel()
|
||||
if (this::job.isInitialized) job?.cancel()
|
||||
if (this::call.isInitialized) call?.cancel()
|
||||
}
|
||||
|
||||
private fun nextRetryMillis(retryMillis: Long, startTime: Long): Long {
|
||||
|
|
|
@ -351,7 +351,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
Log.e(TAG, "Error fetching notifications for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}: ${e.stackTrace}", e)
|
||||
runOnUiThread {
|
||||
Toast
|
||||
.makeText(this@DetailActivity, getString(R.string.refresh_message_error, e.message), Toast.LENGTH_LONG)
|
||||
.makeText(this@DetailActivity, getString(R.string.refresh_message_error_one, e.message), Toast.LENGTH_LONG)
|
||||
.show()
|
||||
mainListContainer.isRefreshing = false
|
||||
}
|
||||
|
|
|
@ -1,23 +1,32 @@
|
|||
package io.heckel.ntfy.ui
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.app.NotificationCompat
|
||||
import android.widget.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.workDataOf
|
||||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.data.Notification
|
||||
import io.heckel.ntfy.msg.AttachmentDownloadWorker
|
||||
import io.heckel.ntfy.msg.NotificationDispatcher
|
||||
import io.heckel.ntfy.util.*
|
||||
import java.util.*
|
||||
|
||||
|
||||
class DetailAdapter(private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
|
||||
ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) {
|
||||
val selected = mutableSetOf<String>() // Notification IDs
|
||||
|
@ -51,13 +60,15 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
|||
private val titleView: TextView = itemView.findViewById(R.id.detail_item_title_text)
|
||||
private val messageView: TextView = itemView.findViewById(R.id.detail_item_message_text)
|
||||
private val newImageView: View = itemView.findViewById(R.id.detail_item_new_dot)
|
||||
private val tagsView: TextView = itemView.findViewById(R.id.detail_item_tags)
|
||||
private val tagsView: TextView = itemView.findViewById(R.id.detail_item_tags_text)
|
||||
private val imageView: ImageView = itemView.findViewById(R.id.detail_item_image)
|
||||
private val attachmentView: TextView = itemView.findViewById(R.id.detail_item_attachment_text)
|
||||
private val menuButton: ImageButton = itemView.findViewById(R.id.detail_item_menu_button)
|
||||
|
||||
fun bind(notification: Notification) {
|
||||
this.notification = notification
|
||||
|
||||
val ctx = itemView.context
|
||||
val context = itemView.context
|
||||
val unmatchedTags = unmatchedTags(splitTags(notification.tags))
|
||||
|
||||
dateView.text = Date(notification.timestamp * 1000).toString()
|
||||
|
@ -73,7 +84,7 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
|||
}
|
||||
if (unmatchedTags.isNotEmpty()) {
|
||||
tagsView.visibility = View.VISIBLE
|
||||
tagsView.text = ctx.getString(R.string.detail_item_tags, unmatchedTags.joinToString(", "))
|
||||
tagsView.text = context.getString(R.string.detail_item_tags, unmatchedTags.joinToString(", "))
|
||||
} else {
|
||||
tagsView.visibility = View.GONE
|
||||
}
|
||||
|
@ -83,29 +94,29 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
|||
when (notification.priority) {
|
||||
1 -> {
|
||||
priorityImageView.visibility = View.VISIBLE
|
||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_1_24dp))
|
||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_1_24dp))
|
||||
}
|
||||
2 -> {
|
||||
priorityImageView.visibility = View.VISIBLE
|
||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_2_24dp))
|
||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_2_24dp))
|
||||
}
|
||||
3 -> {
|
||||
priorityImageView.visibility = View.GONE
|
||||
}
|
||||
4 -> {
|
||||
priorityImageView.visibility = View.VISIBLE
|
||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_4_24dp))
|
||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_4_24dp))
|
||||
}
|
||||
5 -> {
|
||||
priorityImageView.visibility = View.VISIBLE
|
||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_5_24dp))
|
||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_5_24dp))
|
||||
}
|
||||
}
|
||||
// 📄
|
||||
val contentUri = notification.attachment?.contentUri
|
||||
if (contentUri != null && supportedImage(notification.attachment.type)) {
|
||||
val fileExists = if (contentUri != null) fileExists(context, contentUri) else false
|
||||
if (contentUri != null && fileExists && supportedImage(notification.attachment.type)) {
|
||||
try {
|
||||
val resolver = itemView.context.applicationContext.contentResolver
|
||||
val resolver = context.applicationContext.contentResolver
|
||||
val bitmapStream = resolver.openInputStream(Uri.parse(contentUri))
|
||||
val bitmap = BitmapFactory.decodeStream(bitmapStream)
|
||||
imageView.setImageBitmap(bitmap)
|
||||
|
@ -116,6 +127,61 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
|||
} else {
|
||||
imageView.visibility = View.GONE
|
||||
}
|
||||
if (notification.attachment != null) {
|
||||
attachmentView.text = formatAttachmentInfo(notification, fileExists)
|
||||
attachmentView.visibility = View.VISIBLE
|
||||
menuButton.visibility = View.VISIBLE
|
||||
menuButton.setOnClickListener { menuView ->
|
||||
val popup = PopupMenu(context, menuView)
|
||||
popup.menuInflater.inflate(R.menu.menu_detail_attachment, popup.menu)
|
||||
|
||||
val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download)
|
||||
val openItem = popup.menu.findItem(R.id.detail_item_menu_open)
|
||||
val browseItem = popup.menu.findItem(R.id.detail_item_menu_browse)
|
||||
val copyUrlItem = popup.menu.findItem(R.id.detail_item_menu_copy_url)
|
||||
if (contentUri != null && fileExists) {
|
||||
openItem.setOnMenuItemClickListener {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(contentUri))) // FIXME try/catch
|
||||
true
|
||||
}
|
||||
browseItem.setOnMenuItemClickListener {
|
||||
context.startActivity(Intent(DownloadManager.ACTION_VIEW_DOWNLOADS))
|
||||
true
|
||||
}
|
||||
copyUrlItem.setOnMenuItemClickListener {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("attachment url", notification.attachment.url)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast
|
||||
.makeText(context, context.getString(R.string.detail_copied_to_clipboard_message), Toast.LENGTH_LONG)
|
||||
.show()
|
||||
true
|
||||
}
|
||||
downloadItem.isVisible = false
|
||||
} else {
|
||||
openItem.isVisible = false
|
||||
browseItem.isVisible = false
|
||||
downloadItem.setOnMenuItemClickListener {
|
||||
scheduleAttachmentDownload(context, notification)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
popup.show()
|
||||
}
|
||||
} else {
|
||||
attachmentView.visibility = View.GONE
|
||||
menuButton.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleAttachmentDownload(context: Context, notification: Notification) {
|
||||
Log.d(TAG, "Enqueuing work to download attachment")
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
val workRequest = OneTimeWorkRequest.Builder(AttachmentDownloadWorker::class.java)
|
||||
.setInputData(workDataOf("id" to notification.id))
|
||||
.build()
|
||||
workManager.enqueue(workRequest)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,4 +194,8 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
|||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "NtfyDetailAdapter"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,12 +118,12 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
startPeriodicPollWorker()
|
||||
startPeriodicServiceRestartWorker()
|
||||
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
|
||||
/*if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
|
||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1234);
|
||||
}
|
||||
else {
|
||||
Toast.makeText(this, "Permission already granted", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}*/
|
||||
}
|
||||
override fun onRequestPermissionsResult(requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
|
|
|
@ -106,6 +106,26 @@ class SettingsActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
// Auto download
|
||||
val autoDownloadPrefId = context?.getString(R.string.settings_notifications_auto_download_key) ?: return
|
||||
val autoDownload: SwitchPreference? = findPreference(autoDownloadPrefId)
|
||||
autoDownload?.isChecked = repository.getAutoDownloadEnabled()
|
||||
autoDownload?.preferenceDataStore = object : PreferenceDataStore() {
|
||||
override fun putBoolean(key: String?, value: Boolean) {
|
||||
repository.setAutoDownloadEnabled(value)
|
||||
}
|
||||
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||
return repository.getAutoDownloadEnabled()
|
||||
}
|
||||
}
|
||||
autoDownload?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
|
||||
if (pref.isChecked) {
|
||||
getString(R.string.settings_notifications_auto_download_summary_on)
|
||||
} else {
|
||||
getString(R.string.settings_notifications_auto_download_summary_off)
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast enabled
|
||||
val broadcastEnabledPrefId = context?.getString(R.string.settings_advanced_broadcast_key) ?: return
|
||||
val broadcastEnabled: SwitchPreference? = findPreference(broadcastEnabledPrefId)
|
||||
|
|
|
@ -2,8 +2,11 @@ package io.heckel.ntfy.util
|
|||
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.view.Window
|
||||
import io.heckel.ntfy.data.Notification
|
||||
import io.heckel.ntfy.data.PROGRESS_NONE
|
||||
import io.heckel.ntfy.data.Subscription
|
||||
import java.security.SecureRandom
|
||||
import java.text.DateFormat
|
||||
|
@ -106,6 +109,36 @@ fun formatTitle(notification: Notification): String {
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME duplicate code
|
||||
fun formatAttachmentInfo(notification: Notification, fileExists: Boolean): String {
|
||||
if (notification.attachment == null) return ""
|
||||
val att = notification.attachment
|
||||
val infos = mutableListOf<String>()
|
||||
if (att.name != null) infos.add(att.name)
|
||||
if (att.size != null) infos.add(formatBytes(att.size))
|
||||
//if (att.expires != null && att.expires != 0L) infos.add(formatDateShort(att.expires))
|
||||
if (att.progress in 0..99) infos.add("${att.progress}%")
|
||||
if (!fileExists) {
|
||||
if (att.progress == PROGRESS_NONE) infos.add("not downloaded")
|
||||
else infos.add("deleted")
|
||||
}
|
||||
if (infos.size == 0) return ""
|
||||
if (att.progress < 100) return "Downloading ${infos.joinToString(", ")}"
|
||||
return "\uD83D\uDCC4 " + infos.joinToString(", ")
|
||||
}
|
||||
|
||||
// Checks in the most horrible way if a content URI exists; I couldn't find a better way
|
||||
fun fileExists(context: Context, uri: String): Boolean {
|
||||
val resolver = context.applicationContext.contentResolver
|
||||
return try {
|
||||
val fileIS = resolver.openInputStream(Uri.parse(uri))
|
||||
fileIS?.close()
|
||||
true
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785
|
||||
fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) {
|
||||
val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor)
|
||||
|
|
9
app/src/main/res/drawable/ic_more_horiz_gray_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_more_horiz_gray_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"
|
||||
android:fillColor="#555555"/>
|
||||
</vector>
|
|
@ -29,6 +29,14 @@
|
|||
android:layout_marginTop="1dp"
|
||||
app:layout_constraintStart_toEndOf="@id/detail_item_priority_image"
|
||||
android:layout_marginStart="5dp"/>
|
||||
<ImageButton
|
||||
android:layout_width="46dp"
|
||||
android:layout_height="26dp" app:srcCompat="@drawable/ic_more_horiz_gray_24dp"
|
||||
android:id="@+id/detail_item_menu_button"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="5dp"
|
||||
android:background="?android:attr/selectableItemBackground" android:paddingTop="-5dp"
|
||||
android:layout_marginTop="5dp"/>
|
||||
<TextView
|
||||
android:text="This is a very very very long message. It could be as long as 1024 charaters, which is a lot more than you'd think. No, really so far this message is barely 180 characters long. I can't believe how long 1024 bytes are. This is outrageous. Oh you know what, I think I won't type the whole thing. This seems a little too long for a sample text. Well, anyway, it was nice chatting. So far this message is about 400 bytes long. So maybe just double what you see and that's that."
|
||||
android:layout_width="0dp"
|
||||
|
@ -40,16 +48,6 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/detail_item_title_text"
|
||||
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp"
|
||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/detail_item_tags"/>
|
||||
<TextView
|
||||
android:text="Tags: ssh, zfs"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/detail_item_tags"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp"
|
||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/detail_item_message_text"
|
||||
app:layout_constraintBottom_toTopOf="@id/detail_item_image"/>
|
||||
<TextView
|
||||
android:text="This is an optional title. It can also be a little longer but not too long."
|
||||
|
@ -70,13 +68,43 @@
|
|||
app:layout_constraintStart_toEndOf="@+id/detail_item_date_text"
|
||||
app:layout_constraintTop_toTopOf="@+id/detail_item_date_text"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/detail_item_date_text" android:layout_marginStart="5dp"/>
|
||||
<ImageView
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content" app:srcCompat="@drawable/ic_cancel_gray_24dp"
|
||||
android:id="@+id/detail_item_image" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/detail_item_tags"
|
||||
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/detail_item_message_text"
|
||||
android:layout_marginStart="10dp" android:layout_marginEnd="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent" android:scaleType="centerCrop"
|
||||
android:adjustViewBounds="true" android:maxHeight="150dp" android:layout_marginTop="5dp"/>
|
||||
android:scaleType="centerCrop"
|
||||
android:adjustViewBounds="true" android:maxHeight="150dp" android:layout_marginTop="5dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/detail_item_attachment_text" android:layout_marginBottom="5dp"
|
||||
app:shapeAppearanceOverlay="@style/roundedCornersImageView" android:visibility="visible"/>
|
||||
<TextView
|
||||
android:text="📄attachment.jpg, 20.1 KB"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/detail_item_attachment_text"
|
||||
android:textColor="@color/primaryTextColor"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:autoLink="web"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginStart="10dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/detail_item_image"
|
||||
app:layout_constraintBottom_toTopOf="@+id/detail_item_tags_text" android:layout_marginTop="3dp"/>
|
||||
<TextView
|
||||
android:text="Tags: ssh, zfs"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/detail_item_tags_text"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp"
|
||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/detail_item_attachment_text"
|
||||
app:layout_constraintBottom_toTopOf="@id/detail_item_padding_bottom"/>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="10dp" android:id="@+id/detail_item_padding_bottom"
|
||||
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/detail_item_tags_text"
|
||||
app:layout_constraintTop_toBottomOf="@id/detail_item_tags_text"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
7
app/src/main/res/menu/menu_detail_attachment.xml
Normal file
7
app/src/main/res/menu/menu_detail_attachment.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@+id/detail_item_menu_download" android:title="@string/detail_item_menu_download"/>
|
||||
<item android:id="@+id/detail_item_menu_open" android:title="@string/detail_item_menu_open"/>
|
||||
<item android:id="@+id/detail_item_menu_browse" android:title="@string/detail_item_menu_browse"/>
|
||||
<item android:id="@+id/detail_item_menu_copy_url" android:title="@string/detail_item_menu_copy_url"/>
|
||||
</menu>
|
|
@ -24,6 +24,7 @@
|
|||
<string name="refresh_message_result">%1$d notification(s) received</string>
|
||||
<string name="refresh_message_no_results">Everything is up-to-date</string>
|
||||
<string name="refresh_message_error">%1$d subscription(s) could not be refreshed\n\n%2$s</string>
|
||||
<string name="refresh_message_error_one">Subscription could not be refreshed: %1$s</string>
|
||||
|
||||
<!-- Main activity: Action bar -->
|
||||
<string name="main_action_bar_title">Subscribed topics</string>
|
||||
|
@ -108,6 +109,11 @@
|
|||
<string name="detail_instant_delivery_disabled">Instant delivery disabled</string>
|
||||
<string name="detail_instant_info">Instant delivery is enabled</string>
|
||||
<string name="detail_item_tags">Tags: %1$s</string>
|
||||
<string name="detail_item_menu_open">Open file</string>
|
||||
<string name="detail_item_menu_browse">Browse file</string>
|
||||
<string name="detail_item_menu_download">Download file</string>
|
||||
<string name="detail_item_menu_copy_url">Copy URL</string>
|
||||
<string name="detail_item_menu_copy_url_copied">Copied URL to clipboard</string>
|
||||
|
||||
<!-- Detail activity: Action bar -->
|
||||
<string name="detail_menu_notifications_enabled">Notifications enabled</string>
|
||||
|
@ -166,6 +172,10 @@
|
|||
<string name="settings_notifications_min_priority_default">Default priority and higher</string>
|
||||
<string name="settings_notifications_min_priority_high">High priority and higher</string>
|
||||
<string name="settings_notifications_min_priority_max">Only max priority</string>
|
||||
<string name="settings_notifications_auto_download_key">AutoDownload</string>
|
||||
<string name="settings_notifications_auto_download_title">Auto download attachments</string>
|
||||
<string name="settings_notifications_auto_download_summary_on">Attachments are automatically downloaded</string>
|
||||
<string name="settings_notifications_auto_download_summary_off">Attachments are not automatically downloaded</string>
|
||||
<string name="settings_unified_push_header">UnifiedPush</string>
|
||||
<string name="settings_unified_push_header_summary">Allows other apps to use ntfy as a message distributor. Find out more at unifiedpush.org.</string>
|
||||
<string name="settings_unified_push_enabled_key">UnifiedPushEnabled</string>
|
||||
|
|
|
@ -6,4 +6,10 @@
|
|||
<item name="android:statusBarColor">@color/primaryColor</item>
|
||||
<item name="actionModeBackground">@color/primaryDarkColor</item>
|
||||
</style>
|
||||
|
||||
<!-- Rounded corners in images: https://stackoverflow.com/a/61960983/1440785 -->
|
||||
<style name="roundedCornersImageView" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">5dp</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
@ -13,6 +13,10 @@
|
|||
app:entries="@array/settings_notifications_min_priority_entries"
|
||||
app:entryValues="@array/settings_notifications_min_priority_values"
|
||||
app:defaultValue="1"/>
|
||||
<SwitchPreference
|
||||
app:key="@string/settings_notifications_auto_download_key"
|
||||
app:title="@string/settings_notifications_auto_download_title"
|
||||
app:enabled="true"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
app:title="@string/settings_unified_push_header"
|
||||
|
|
|
@ -8,6 +8,7 @@ import io.heckel.ntfy.R
|
|||
import io.heckel.ntfy.app.Application
|
||||
import io.heckel.ntfy.data.Attachment
|
||||
import io.heckel.ntfy.data.Notification
|
||||
import io.heckel.ntfy.data.PROGRESS_NONE
|
||||
import io.heckel.ntfy.msg.*
|
||||
import io.heckel.ntfy.service.SubscriberService
|
||||
import io.heckel.ntfy.util.toPriority
|
||||
|
|
1
assets/more_horiz_black_24dp.svg
Normal file
1
assets/more_horiz_black_24dp.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
|
After Width: | Height: | Size: 306 B |
Loading…
Reference in a new issue