Auto-download max size

This commit is contained in:
Philipp Heckel 2022-01-11 19:37:34 -05:00
parent 40d8d20cc5
commit 5a6d45d810
10 changed files with 98 additions and 44 deletions

View file

@ -162,14 +162,18 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
return sharedPrefs.getInt(SHARED_PREFS_MIN_PRIORITY, 1) // 1/low means all priorities return sharedPrefs.getInt(SHARED_PREFS_MIN_PRIORITY, 1) // 1/low means all priorities
} }
fun getAutoDownloadEnabled(): Boolean { fun getAutoDownloadMaxSize(): Long {
val defaultEnabled = Build.VERSION.SDK_INT > Build.VERSION_CODES.P // Need to request permission on older versions val defaultValue = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
return sharedPrefs.getBoolean(SHARED_PREFS_AUTO_DOWNLOAD_ENABLED, defaultEnabled) AUTO_DOWNLOAD_NEVER // Need to request permission on older versions
} else {
AUTO_DOWNLOAD_DEFAULT
}
return sharedPrefs.getLong(SHARED_PREFS_AUTO_DOWNLOAD_MAX_SIZE, defaultValue)
} }
fun setAutoDownloadEnabled(enabled: Boolean) { fun setAutoDownloadMaxSize(maxSize: Long) {
sharedPrefs.edit() sharedPrefs.edit()
.putBoolean(SHARED_PREFS_AUTO_DOWNLOAD_ENABLED, enabled) .putLong(SHARED_PREFS_AUTO_DOWNLOAD_MAX_SIZE, maxSize)
.apply() .apply()
} }
@ -303,12 +307,14 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion" const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion"
const val SHARED_PREFS_MUTED_UNTIL_TIMESTAMP = "MutedUntil" const val SHARED_PREFS_MUTED_UNTIL_TIMESTAMP = "MutedUntil"
const val SHARED_PREFS_MIN_PRIORITY = "MinPriority" const val SHARED_PREFS_MIN_PRIORITY = "MinPriority"
const val SHARED_PREFS_AUTO_DOWNLOAD_ENABLED = "AutoDownload" const val SHARED_PREFS_AUTO_DOWNLOAD_MAX_SIZE = "AutoDownload"
const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled" const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled"
const val SHARED_PREFS_UNIFIED_PUSH_ENABLED = "UnifiedPushEnabled" const val SHARED_PREFS_UNIFIED_PUSH_ENABLED = "UnifiedPushEnabled"
const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL" const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL"
const val PREVIEWS_CACHE_DIR = "Previews" const val AUTO_DOWNLOAD_NEVER = 0L
const val AUTO_DOWNLOAD_ALWAYS = 1L
const val AUTO_DOWNLOAD_DEFAULT = 1024 * 1024L // Must match a value in values.xml
private const val TAG = "NtfyRepository" private const val TAG = "NtfyRepository"
private var instance: Repository? = null private var instance: Repository? = null

View file

@ -7,6 +7,7 @@ import com.google.gson.Gson
import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.data.Attachment import io.heckel.ntfy.data.Attachment
import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.PROGRESS_NONE
import io.heckel.ntfy.util.* import io.heckel.ntfy.util.*
import okhttp3.* import okhttp3.*
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
@ -54,11 +55,7 @@ class ApiService {
} }
} }
fun poll(subscriptionId: Long, baseUrl: String, topic: String): List<Notification> { fun poll(subscriptionId: Long, baseUrl: String, topic: String, since: Long = 0L): List<Notification> {
return poll(subscriptionId, baseUrl, topic, 0)
}
fun poll(subscriptionId: Long, baseUrl: String, topic: String, since: Long): List<Notification> {
val sinceVal = if (since == 0L) "all" else since.toString() val sinceVal = if (since == 0L) "all" else since.toString()
val url = topicUrlJsonPoll(baseUrl, topic, sinceVal) val url = topicUrlJsonPoll(baseUrl, topic, sinceVal)
Log.d(TAG, "Polling topic $url") Log.d(TAG, "Polling topic $url")

View file

@ -119,6 +119,7 @@ class DownloadWorker(private val context: Context, params: WorkerParameters) : W
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Attachment download failed", e) Log.w(TAG, "Attachment download failed", e)
// Mark attachment download as failed
val newAttachment = attachment.copy(progress = PROGRESS_FAILED) val newAttachment = attachment.copy(progress = PROGRESS_FAILED)
val newNotification = notification.copy(attachment = newAttachment) val newNotification = notification.copy(attachment = newAttachment)
notifier.update(subscription, newNotification) notifier.update(subscription, newNotification)

View file

@ -2,9 +2,6 @@ package io.heckel.ntfy.msg
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.workDataOf
import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.data.Subscription
@ -31,7 +28,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
val notify = shouldNotify(subscription, notification, muted) val notify = shouldNotify(subscription, notification, muted)
val broadcast = shouldBroadcast(subscription) val broadcast = shouldBroadcast(subscription)
val distribute = shouldDistribute(subscription) val distribute = shouldDistribute(subscription)
val download = shouldDownload(subscription, notification) val download = shouldDownload(notification)
if (notify) { if (notify) {
notifier.display(subscription, notification) notifier.display(subscription, notification)
} }
@ -48,8 +45,21 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
} }
} }
private fun shouldDownload(subscription: Subscription, notification: Notification): Boolean { private fun shouldDownload(notification: Notification): Boolean {
return notification.attachment != null && repository.getAutoDownloadEnabled() if (notification.attachment == null) {
return false
}
val maxAutoDownloadSize = repository.getAutoDownloadMaxSize()
when (maxAutoDownloadSize) {
Repository.AUTO_DOWNLOAD_ALWAYS -> return true
Repository.AUTO_DOWNLOAD_NEVER -> return false
else -> {
if (notification.attachment.size == null) {
return false
}
return notification.attachment.size <= maxAutoDownloadSize
}
}
} }
private fun shouldNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean { private fun shouldNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean {

View file

@ -153,8 +153,10 @@ class SubscriberService : Service() {
// Start new connections and restart connections (if subscriptions have changed) // Start new connections and restart connections (if subscriptions have changed)
instantSubscriptionsByBaseUrl.forEach { (baseUrl, subscriptions) -> instantSubscriptionsByBaseUrl.forEach { (baseUrl, subscriptions) ->
// Do NOT request old messages for new connections; we'll call poll() in MainActivity.
// This is important, so we don't download attachments from old messages, which is not desired.
var since = System.currentTimeMillis()/1000
val connection = connections[baseUrl] val connection = connections[baseUrl]
var since = 0L
if (connection != null && !connection.matches(subscriptions.values)) { if (connection != null && !connection.matches(subscriptions.values)) {
since = connection.since() since = connection.since()
connections.remove(baseUrl) connections.remove(baseUrl)

View file

@ -20,6 +20,7 @@ import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.util.formatBytes
import io.heckel.ntfy.util.formatDateShort import io.heckel.ntfy.util.formatDateShort
import io.heckel.ntfy.util.toPriorityString import io.heckel.ntfy.util.toPriorityString
@ -49,6 +50,8 @@ class SettingsActivity : AppCompatActivity() {
} }
class SettingsFragment(val repository: Repository, private val supportFragmentManager: FragmentManager) : PreferenceFragmentCompat() { class SettingsFragment(val repository: Repository, private val supportFragmentManager: FragmentManager) : PreferenceFragmentCompat() {
private var autoDownloadSelection = repository.getAutoDownloadMaxSize() // Only used for <= Android P, due to permissions request
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.main_preferences, rootKey) setPreferencesFromResource(R.xml.main_preferences, rootKey)
@ -115,27 +118,30 @@ class SettingsActivity : AppCompatActivity() {
// Auto download // Auto download
val autoDownloadPrefId = context?.getString(R.string.settings_notifications_auto_download_key) ?: return val autoDownloadPrefId = context?.getString(R.string.settings_notifications_auto_download_key) ?: return
val autoDownload: SwitchPreference? = findPreference(autoDownloadPrefId) val autoDownload: ListPreference? = findPreference(autoDownloadPrefId)
autoDownload?.isChecked = repository.getAutoDownloadEnabled() autoDownload?.value = repository.getAutoDownloadMaxSize().toString()
autoDownload?.preferenceDataStore = object : PreferenceDataStore() { autoDownload?.preferenceDataStore = object : PreferenceDataStore() {
override fun putBoolean(key: String?, value: Boolean) { override fun putString(key: String?, value: String?) {
repository.setAutoDownloadEnabled(value) val maxSize = value?.toLongOrNull() ?:return
repository.setAutoDownloadMaxSize(maxSize)
} }
override fun getBoolean(key: String?, defValue: Boolean): Boolean { override fun getString(key: String?, defValue: String?): String {
return repository.getAutoDownloadEnabled() return repository.getAutoDownloadMaxSize().toString()
} }
} }
autoDownload?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref -> autoDownload?.summaryProvider = Preference.SummaryProvider<ListPreference> { pref ->
if (pref.isChecked) { val maxSize = pref.value.toLongOrNull() ?: repository.getAutoDownloadMaxSize()
getString(R.string.settings_notifications_auto_download_summary_on) when (maxSize) {
} else { Repository.AUTO_DOWNLOAD_NEVER -> getString(R.string.settings_notifications_auto_download_summary_never)
getString(R.string.settings_notifications_auto_download_summary_off) Repository.AUTO_DOWNLOAD_ALWAYS -> getString(R.string.settings_notifications_auto_download_summary_always)
else -> getString(R.string.settings_notifications_auto_download_summary_smaller_than_x, formatBytes(maxSize, decimals = 0))
} }
} }
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
autoDownload?.setOnPreferenceChangeListener { _, v -> autoDownload?.setOnPreferenceChangeListener { _, v ->
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD) ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD)
autoDownloadSelection = v.toString().toLongOrNull() ?: repository.getAutoDownloadMaxSize()
false // If permission is granted, auto-download will be enabled in onRequestPermissionsResult() false // If permission is granted, auto-download will be enabled in onRequestPermissionsResult()
} else { } else {
true true
@ -222,10 +228,11 @@ class SettingsActivity : AppCompatActivity() {
} }
} }
fun enableAutoDownload() { fun setAutoDownload() {
val autoDownloadPrefId = context?.getString(R.string.settings_notifications_auto_download_key) ?: return val autoDownloadPrefId = context?.getString(R.string.settings_notifications_auto_download_key) ?: return
val autoDownload: SwitchPreference? = findPreference(autoDownloadPrefId) val autoDownload: ListPreference? = findPreference(autoDownloadPrefId)
autoDownload?.isChecked = true autoDownload?.value = autoDownloadSelection.toString()
repository.setAutoDownloadMaxSize(autoDownloadSelection)
} }
} }
@ -233,15 +240,14 @@ class SettingsActivity : AppCompatActivity() {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD) { if (requestCode == REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
enableAutoDownload() setAutoDownload()
repository.setAutoDownloadEnabled(true)
} }
} }
} }
private fun enableAutoDownload() { private fun setAutoDownload() {
if (!this::fragment.isInitialized) return if (!this::fragment.isInitialized) return
fragment.enableAutoDownload() fragment.setAutoDownload()
} }
companion object { companion object {

View file

@ -13,6 +13,7 @@ import java.security.SecureRandom
import java.text.DateFormat import java.text.DateFormat
import java.text.StringCharacterIterator import java.text.StringCharacterIterator
import java.util.* import java.util.*
import kotlin.math.abs
fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}" fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}"
fun topicUrlUp(baseUrl: String, topic: String) = "${baseUrl}/${topic}?up=1" // UnifiedPush fun topicUrlUp(baseUrl: String, topic: String) = "${baseUrl}/${topic}?up=1" // UnifiedPush
@ -163,8 +164,8 @@ inline fun <T1: Any, T2: Any, R: Any> safeLet(p1: T1?, p2: T2?, block: (T1, T2)-
return if (p1 != null && p2 != null) block(p1, p2) else null return if (p1 != null && p2 != null) block(p1, p2) else null
} }
fun formatBytes(bytes: Long): String { fun formatBytes(bytes: Long, decimals: Int = 1): String {
val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else Math.abs(bytes) val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else abs(bytes)
if (absB < 1024) { if (absB < 1024) {
return "$bytes B" return "$bytes B"
} }
@ -177,7 +178,7 @@ fun formatBytes(bytes: Long): String {
i -= 10 i -= 10
} }
value *= java.lang.Long.signum(bytes).toLong() value *= java.lang.Long.signum(bytes).toLong()
return java.lang.String.format("%.1f %cB", value / 1024.0, ci.current()) return java.lang.String.format("%.${decimals}f %cB", value / 1024.0, ci.current())
} }
fun supportedImage(mimeType: String?): Boolean { fun supportedImage(mimeType: String?): Boolean {

View file

@ -193,8 +193,17 @@
<string name="settings_notifications_min_priority_max">Only max priority</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_key">AutoDownload</string>
<string name="settings_notifications_auto_download_title">Auto download attachments</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_always">Attachments are always downloaded automatically</string>
<string name="settings_notifications_auto_download_summary_off">Attachments are not automatically downloaded</string> <string name="settings_notifications_auto_download_summary_never">Attachments are never downloaded automatically</string>
<string name="settings_notifications_auto_download_summary_smaller_than_x">Attachments up to %1$s are downloaded automatically</string>
<string name="settings_notifications_auto_download_never">Never download automatically</string>
<string name="settings_notifications_auto_download_always">Always download automatically</string>
<string name="settings_notifications_auto_download_100k">If smaller than 100 KB</string>
<string name="settings_notifications_auto_download_500k">If smaller than 500 KB</string>
<string name="settings_notifications_auto_download_1m">If smaller than 1 MB</string>
<string name="settings_notifications_auto_download_5m">If smaller than 5 MB</string>
<string name="settings_notifications_auto_download_10m">If smaller than 10 MB</string>
<string name="settings_notifications_auto_download_50m">If smaller than 50 MB</string>
<string name="settings_unified_push_header">UnifiedPush</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_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> <string name="settings_unified_push_enabled_key">UnifiedPushEnabled</string>

View file

@ -14,4 +14,24 @@
<item>4</item> <item>4</item>
<item>5</item> <item>5</item>
</string-array> </string-array>
<string-array name="settings_notifications_auto_download_entries">
<item>@string/settings_notifications_auto_download_never</item>
<item>@string/settings_notifications_auto_download_always</item>
<item>@string/settings_notifications_auto_download_100k</item>
<item>@string/settings_notifications_auto_download_500k</item>
<item>@string/settings_notifications_auto_download_1m</item>
<item>@string/settings_notifications_auto_download_5m</item>
<item>@string/settings_notifications_auto_download_10m</item>
<item>@string/settings_notifications_auto_download_50m</item>
</string-array>
<string-array name="settings_notifications_auto_download_values">
<item>0</item>
<item>1</item>
<item>102400</item>
<item>512000</item>
<item>1048576</item>
<item>5242880</item>
<item>10485760</item>
<item>52428800</item>
</string-array>
</resources> </resources>

View file

@ -13,10 +13,12 @@
app:entries="@array/settings_notifications_min_priority_entries" app:entries="@array/settings_notifications_min_priority_entries"
app:entryValues="@array/settings_notifications_min_priority_values" app:entryValues="@array/settings_notifications_min_priority_values"
app:defaultValue="1"/> app:defaultValue="1"/>
<SwitchPreference <ListPreference
app:key="@string/settings_notifications_auto_download_key" app:key="@string/settings_notifications_auto_download_key"
app:title="@string/settings_notifications_auto_download_title" app:title="@string/settings_notifications_auto_download_title"
app:enabled="true"/> app:entries="@array/settings_notifications_auto_download_entries"
app:entryValues="@array/settings_notifications_auto_download_values"
app:defaultValue="1"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
app:title="@string/settings_unified_push_header" app:title="@string/settings_unified_push_header"