Merge branch 'main' into custom_notification_channels

This commit is contained in:
Philipp Heckel 2022-12-06 20:45:12 -05:00
commit 6f0bf4d112
41 changed files with 461 additions and 138 deletions

View file

@ -14,8 +14,8 @@ android {
minSdkVersion 21
targetSdkVersion 33
versionCode 29
versionName "1.15.0"
versionCode 32
versionName "1.16.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -44,10 +44,12 @@ android {
play {
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'true'
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'true'
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'false'
}
fdroid {
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false'
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false'
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true'
}
}
@ -64,12 +66,29 @@ android {
}
}
// Disables GoogleServices tasks for F-Droid variant
android.applicationVariants.all { variant ->
def shouldProcessGoogleServices = variant.flavorName == "play"
def googleTask = tasks.findByName("process${variant.name.capitalize()}GoogleServices")
googleTask.enabled = shouldProcessGoogleServices
}
// Strips out REQUEST_INSTALL_PACKAGES permission for Google Play variant
android.applicationVariants.all { variant ->
def shouldStripInstallPermission = variant.flavorName == "play"
if (shouldStripInstallPermission) {
variant.outputs.each { output ->
def processManifest = output.getProcessManifestProvider().get()
processManifest.doLast { task ->
def outputDir = task.getMultiApkManifestOutputDirectory().get().asFile
def manifestOutFile = file("$outputDir/AndroidManifest.xml")
def newFileContents = manifestOutFile.collect { s -> s.contains("android.permission.REQUEST_INSTALL_PACKAGES") ? "" : s }.join("\n")
manifestOutFile.write(newFileContents, 'UTF-8')
}
}
}
}
dependencies {
// AndroidX, The Basics
implementation "androidx.appcompat:appcompat:1.5.1"

View file

@ -1,5 +1,6 @@
package io.heckel.ntfy.firebase
@Suppress("UNUSED_PARAMETER")
class FirebaseMessenger {
fun subscribe(topic: String) {
// Dummy to keep F-Droid flavor happy

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.heckel.ntfy">
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- For instant delivery foregrounds service -->
@ -8,10 +9,17 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <!-- To restart service on reboot -->
<uses-permission android:name="android.permission.VIBRATE"/> <!-- Incoming notifications should be able to vibrate the phone -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/> <!-- Only required on SDK <= 28 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <!-- To install packages downloaded through ntfy; craazyy! -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <!-- To reschedule the websocket retry -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- As of Android 13, we need to ask for permission to post notifications -->
<!--
Permission REQUEST_INSTALL_PACKAGES (F-Droid only!):
- Permission is used to install .apk files that were received as attachments
- Google rejected the permission for ntfy, so this permission is STRIPPED OUT by the build process
for the Google Play variant of the app.
-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application
android:name=".app.Application"
android:allowBackup="true"

View file

@ -1,7 +1,6 @@
package io.heckel.ntfy.app
import android.app.Application
import android.content.Context
import io.heckel.ntfy.db.Database
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.util.Log

View file

@ -89,7 +89,7 @@ class Backuper(val context: Context) {
private suspend fun applySubscriptions(subscriptions: List<Subscription>?) {
if (subscriptions == null) {
return;
return
}
val appBaseUrl = context.getString(R.string.app_base_url)
subscriptions.forEach { s ->
@ -120,7 +120,7 @@ class Backuper(val context: Context) {
private suspend fun applyNotifications(notifications: List<Notification>?) {
if (notifications == null) {
return;
return
}
notifications.forEach { n ->
try {
@ -189,7 +189,7 @@ class Backuper(val context: Context) {
private suspend fun applyUsers(users: List<User>?) {
if (users == null) {
return;
return
}
users.forEach { u ->
try {

View file

@ -95,7 +95,7 @@ class ApiService {
throw Exception("Unexpected response ${response.code} when polling topic $url")
}
val body = response.body?.string()?.trim()
if (body == null || body.isEmpty()) return emptyList()
if (body.isNullOrEmpty()) return emptyList()
val notifications = body.lines().mapNotNull { line ->
parser.parse(line, subscriptionId = subscriptionId, notificationId = 0) // No notification when we poll
}
@ -166,7 +166,7 @@ class ApiService {
}
class UnauthorizedException(val user: User?) : Exception()
class EntityTooLargeException() : Exception()
class EntityTooLargeException : Exception()
companion object {
val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})"

View file

@ -11,7 +11,7 @@ import io.heckel.ntfy.util.Log
* Download attachment in the background via WorkManager
*
* The indirection via WorkManager is required since this code may be executed
* in a doze state and Internet may not be available. It's also best practice apparently.
* in a doze state and Internet may not be available. It's also best practice, apparently.
*/
object DownloadManager {
private const val TAG = "NtfyDownloadManager"

View file

@ -206,6 +206,9 @@ class NotificationService(val context: Context) {
}
private fun maybeAddOpenAction(builder: NotificationCompat.Builder, notification: Notification) {
if (!canOpenAttachment(notification.attachment)) {
return
}
if (notification.attachment?.contentUri != null) {
val contentUri = Uri.parse(notification.attachment.contentUri)
val intent = Intent(Intent.ACTION_VIEW, contentUri).apply {

View file

@ -200,7 +200,7 @@ class SubscriberService : Service() {
// retrieve old messages. This is important, so we don't download attachments from old messages.
val since = sinceByBaseUrl[connectionId.baseUrl] ?: "none"
val serviceActive = { -> isServiceStarted }
val serviceActive = { isServiceStarted }
val user = repository.getUser(connectionId.baseUrl)
val connection = if (repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS) {
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
@ -310,11 +310,11 @@ class SubscriberService : Service() {
override fun onTaskRemoved(rootIntent: Intent) {
val restartServiceIntent = Intent(applicationContext, SubscriberService::class.java).also {
it.setPackage(packageName)
};
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE);
applicationContext.getSystemService(Context.ALARM_SERVICE);
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
}
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE)
applicationContext.getSystemService(Context.ALARM_SERVICE)
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent)
}
/* This re-starts the service on reboot; see manifest */

View file

@ -5,8 +5,6 @@ import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager

View file

@ -7,6 +7,7 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.Html
import android.view.ActionMode
@ -179,7 +180,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
howToExample.linksClickable = true
val howToText = getString(R.string.detail_how_to_example, topicUrl)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
howToExample.text = Html.fromHtml(howToText, Html.FROM_HTML_MODE_LEGACY)
} else {
howToExample.text = Html.fromHtml(howToText)
@ -242,7 +243,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == 0) {
Log.d(TAG, "$itemCount item(s) inserted at $positionStart, scrolling to the top")
Log.d(TAG, "$itemCount item(s) inserted at 0, scrolling to the top")
mainList.scrollToPosition(positionStart)
}
}
@ -572,7 +573,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
dialog.setOnShowListener {
dialog
.getButton(AlertDialog.BUTTON_POSITIVE)
.setTextAppearance(R.style.DangerText)
.dangerButton(this)
}
dialog.show()
}
@ -610,7 +611,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
dialog.setOnShowListener {
dialog
.getButton(AlertDialog.BUTTON_POSITIVE)
.setTextAppearance(R.style.DangerText)
.dangerButton(this)
}
dialog.show()
}
@ -620,7 +621,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
handleActionModeClick(notification)
} else if (notification.click != "") {
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(notification.click)))
startActivity(Intent(ACTION_VIEW, Uri.parse(notification.click)))
} catch (e: Exception) {
Log.w(TAG, "Cannot open click URL", e)
runOnUiThread {
@ -721,7 +722,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
dialog.setOnShowListener {
dialog
.getButton(AlertDialog.BUTTON_POSITIVE)
.setTextAppearance(R.style.DangerText)
.dangerButton(this)
}
dialog.show()
}

View file

@ -27,14 +27,16 @@ import com.google.android.material.button.MaterialButton
import com.stfalcon.imageviewer.StfalconImageViewer
import io.heckel.ntfy.R
import io.heckel.ntfy.db.*
import io.heckel.ntfy.msg.DownloadManager
import io.heckel.ntfy.msg.DownloadAttachmentWorker
import io.heckel.ntfy.msg.DownloadManager
import io.heckel.ntfy.msg.DownloadType
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW
import io.heckel.ntfy.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class DetailAdapter(private val activity: Activity, private val lifecycleScope: CoroutineScope, private val repository: Repository, private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) {
@ -204,7 +206,7 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
}
private fun maybeRenderActions(context: Context, notification: Notification) {
if (notification.actions != null && notification.actions.isNotEmpty()) {
if (!notification.actions.isNullOrEmpty()) {
actionsWrapperView.visibility = View.VISIBLE
val actionsCount = Math.min(notification.actions.size, 3) // per documentation, only 3 actions are available
for (i in 0 until actionsCount) {
@ -220,7 +222,7 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
private fun resetCardButtons() {
// clear any previously created dynamic buttons
actionsFlow.allViews.forEach { it -> actionsFlow.removeView(it) }
actionsFlow.allViews.forEach { actionsFlow.removeView(it) }
actionsWrapperView.removeAllViews()
actionsWrapperView.addView(actionsFlow)
}
@ -371,6 +373,12 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
}
private fun openFile(context: Context, attachment: Attachment): Boolean {
if (!canOpenAttachment(attachment)) {
Toast
.makeText(context, context.getString(R.string.detail_item_cannot_open_apk), Toast.LENGTH_LONG)
.show()
return true
}
Log.d(TAG, "Opening file ${attachment.contentUri}")
try {
val contentUri = Uri.parse(attachment.contentUri)

View file

@ -229,9 +229,8 @@ class DetailSettingsActivity : AppCompatActivity() {
return subscription.mutedUntil.toString()
}
}
pref?.summaryProvider = Preference.SummaryProvider<ListPreference> { _ ->
val mutedUntilValue = subscription.mutedUntil
when (mutedUntilValue) {
pref?.summaryProvider = Preference.SummaryProvider<ListPreference> {
when (val mutedUntilValue = subscription.mutedUntil) {
Repository.MUTED_UNTIL_SHOW_ALL -> getString(R.string.settings_notifications_muted_until_show_all)
Repository.MUTED_UNTIL_FOREVER -> getString(R.string.settings_notifications_muted_until_forever)
else -> {
@ -312,7 +311,7 @@ class DetailSettingsActivity : AppCompatActivity() {
iconSetPref = findPreference(prefId) ?: return
iconSetPref.isVisible = subscription.icon == null
iconSetPref.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
iconSetPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
iconSetPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
iconSetLauncher.launch("image/*")
true
}
@ -323,7 +322,7 @@ class DetailSettingsActivity : AppCompatActivity() {
iconRemovePref = findPreference(prefId) ?: return
iconRemovePref.isVisible = subscription.icon != null
iconRemovePref.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
iconRemovePref.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
iconRemovePref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
iconRemovePref.isVisible = false
iconSetPref.isVisible = true
deleteIcon(subscription.icon)

View file

@ -35,7 +35,6 @@ import io.heckel.ntfy.app.Application
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.DownloadManager
import io.heckel.ntfy.msg.DownloadType
@ -48,7 +47,6 @@ import io.heckel.ntfy.work.PollWorker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.security.SecureRandom
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.random.Random
@ -623,7 +621,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
dialog.setOnShowListener {
dialog
.getButton(AlertDialog.BUTTON_POSITIVE)
.setTextAppearance(R.style.DangerText)
.dangerButton(this)
}
dialog.show()
}

View file

@ -1,9 +1,7 @@
package io.heckel.ntfy.ui
import android.content.Context
import android.graphics.BitmapFactory
import android.graphics.Color
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -17,10 +15,8 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.db.ConnectionState
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.util.readBitmapFromUriOrNull
import io.heckel.ntfy.util.displayName
import io.heckel.ntfy.util.readBitmapFromUriOrNull
import java.text.DateFormat
import java.util.*
@ -119,7 +115,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
if (selected.contains(subscription.id)) {
itemView.setBackgroundResource(Colors.itemSelectedBackground(context))
} else {
itemView.setBackgroundColor(Color.TRANSPARENT);
itemView.setBackgroundColor(Color.TRANSPARENT)
}
}
}

View file

@ -8,10 +8,8 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import io.heckel.ntfy.db.*
import io.heckel.ntfy.up.Distributor
import io.heckel.ntfy.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.collections.List
class SubscriptionsViewModel(private val repository: Repository) : ViewModel() {
fun list(): LiveData<List<Subscription>> {

View file

@ -8,7 +8,6 @@ import android.widget.RadioButton
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import io.heckel.ntfy.R
import io.heckel.ntfy.db.Database
import io.heckel.ntfy.db.Repository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay

View file

@ -1,7 +1,6 @@
package io.heckel.ntfy.ui
import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
@ -295,7 +294,7 @@ class ShareActivity : AppCompatActivity() {
.show()
}
} catch (e: Exception) {
val message = if (e is ApiService.UnauthorizedException) {
val errorMessage = if (e is ApiService.UnauthorizedException) {
if (e.user != null) {
getString(R.string.detail_test_message_error_unauthorized_user, e.user.username)
} else {
@ -308,7 +307,7 @@ class ShareActivity : AppCompatActivity() {
}
runOnUiThread {
progress.visibility = View.GONE
errorText.text = message
errorText.text = errorMessage
errorImage.visibility = View.VISIBLE
errorText.visibility = View.VISIBLE
}

View file

@ -3,19 +3,17 @@ package io.heckel.ntfy.ui
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.WindowManager
import android.widget.Button
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputEditText
import io.heckel.ntfy.R
import io.heckel.ntfy.db.User
import io.heckel.ntfy.util.AfterChangedTextWatcher
import io.heckel.ntfy.util.dangerButton
import io.heckel.ntfy.util.validUrl
class UserFragment : DialogFragment() {
@ -98,28 +96,14 @@ class UserFragment : DialogFragment() {
// Delete button should be red
if (user != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
dialog
.getButton(AlertDialog.BUTTON_NEUTRAL)
.setTextAppearance(R.style.DangerText)
} else {
dialog
.getButton(AlertDialog.BUTTON_NEUTRAL)
.setTextColor(ContextCompat.getColor(requireContext(), Colors.dangerText(requireContext())))
}
dialog
.getButton(AlertDialog.BUTTON_NEUTRAL)
.dangerButton(requireContext())
}
// Validate input when typing
val textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
validateInput()
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// Nothing
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// Nothing
}
val textWatcher = AfterChangedTextWatcher {
validateInput()
}
baseUrlView.addTextChangedListener(textWatcher)
usernameView.addTextChangedListener(textWatcher)
@ -140,7 +124,7 @@ class UserFragment : DialogFragment() {
}
// Show keyboard when the dialog is shown (see https://stackoverflow.com/a/19573049/1440785)
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
return dialog
}

View file

@ -10,12 +10,10 @@ import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.*
import kotlin.random.Random
/**
* This is the UnifiedPush broadcast receiver to handle the distributor actions REGISTER and UNREGISTER.

View file

@ -22,12 +22,16 @@ import android.util.Base64
import android.util.TypedValue
import android.view.View
import android.view.Window
import android.widget.Button
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R
import io.heckel.ntfy.db.*
import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64
import io.heckel.ntfy.ui.Colors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -221,16 +225,6 @@ fun maybeAppendActionErrors(message: String, notification: Notification): String
}
}
// Checks in the most horrible way if a content URI exists; I couldn't find a better way
fun fileExists(context: Context, contentUri: String?): Boolean {
return try {
fileStat(context, Uri.parse(contentUri)) // Throws if the file does not exist
true
} catch (_: Exception) {
false
}
}
// Queries the filename of a content URI
fun fileName(context: Context, contentUri: String?, fallbackName: String): String {
return try {
@ -325,6 +319,8 @@ fun formatBytes(bytes: Long, decimals: Int = 1): String {
return java.lang.String.format("%.${decimals}f %cB", value / 1024.0, ci.current())
}
const val androidAppMimeType = "application/vnd.android.package-archive"
fun mimeTypeToIconResource(mimeType: String?): Int {
return if (mimeType?.startsWith("image/") == true) {
R.drawable.ic_file_image_red_24dp
@ -332,7 +328,7 @@ fun mimeTypeToIconResource(mimeType: String?): Int {
R.drawable.ic_file_video_orange_24dp
} else if (mimeType?.startsWith("audio/") == true) {
R.drawable.ic_file_audio_purple_24dp
} else if (mimeType == "application/vnd.android.package-archive") {
} else if (mimeType == androidAppMimeType) {
R.drawable.ic_file_app_gray_24dp
} else {
R.drawable.ic_file_document_blue_24dp
@ -343,6 +339,15 @@ fun supportedImage(mimeType: String?): Boolean {
return listOf("image/jpeg", "image/png").contains(mimeType)
}
// Google Play doesn't allow us to install received .apk files anymore.
// See https://github.com/binwiederhier/ntfy/issues/531
fun canOpenAttachment(attachment: Attachment?): Boolean {
if (attachment?.type == androidAppMimeType && !BuildConfig.INSTALL_PACKAGES_AVAILABLE) {
return false
}
return true
}
// Check if battery optimization is enabled, see https://stackoverflow.com/a/49098293/1440785
fun isIgnoringBatteryOptimizations(context: Context): Boolean {
val powerManager = context.applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
@ -494,3 +499,11 @@ fun String.sha256(): String {
val digest = md.digest(this.toByteArray())
return digest.fold("") { str, it -> str + "%02x".format(it) }
}
fun Button.dangerButton(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setTextAppearance(R.style.DangerText)
} else {
setTextColor(ContextCompat.getColor(context, Colors.dangerText(context)))
}
}

View file

@ -327,4 +327,6 @@
<string name="detail_settings_about_header">Относно</string>
<string name="detail_settings_about_topic_url_title">Адрес на темата</string>
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">Копирано в междинната памет</string>
<string name="main_menu_donate_title">Даряване 💸</string>
<string name="detail_item_cannot_open_apk">Ntfy не може да инсталира получени приложения. Вместо това изтеглете чрез браузъра. За подробности вижте дефект №531.</string>
</resources>

View file

@ -6,4 +6,43 @@
<string name="channel_notifications_high_name">Notificacions (alta prioritat)</string>
<string name="channel_notifications_max_name">Notificacions (prioritat màx)</string>
<string name="channel_subscriber_service_name">Servei Subscripció</string>
<string name="main_menu_donate_title">Donar 💸</string>
<string name="main_item_status_reconnecting">reconnectant…</string>
<string name="channel_subscriber_notification_title">Escoltant notificacions entrants</string>
<string name="channel_subscriber_notification_instant_text">Subscrit per entrega instantània de temes</string>
<string name="channel_subscriber_notification_instant_text_one">Subscrit per entrega instantània d\'un tema</string>
<string name="channel_subscriber_notification_instant_text_two">Subscrit per dues entregues instantànies de temes</string>
<string name="channel_subscriber_notification_instant_text_three">Subscrit per tres entregues instantànies de temes</string>
<string name="channel_subscriber_notification_instant_text_four">Subscrit per quatre entregues instantànies de temes</string>
<string name="channel_subscriber_notification_instant_text_five">Subscrit per cinc entregues instantànies de temes</string>
<string name="channel_subscriber_notification_instant_text_six">Subscrit per sis entregues instantànies de temes</string>
<string name="channel_subscriber_notification_instant_text_more">Subscrit per %1$d entregues instantànies de temes</string>
<string name="channel_subscriber_notification_noinstant_text">Subscrit als temes</string>
<string name="channel_subscriber_notification_noinstant_text_one">Subscrit a un tema</string>
<string name="channel_subscriber_notification_noinstant_text_two">Subscrit a dos temes</string>
<string name="channel_subscriber_notification_noinstant_text_three">Subscrit a tres temes</string>
<string name="channel_subscriber_notification_noinstant_text_six">Subscrit a sis temes</string>
<string name="main_action_mode_delete_dialog_permanently_delete">Eliminat permanentment</string>
<string name="main_action_mode_delete_dialog_cancel">Cancel·lar</string>
<string name="main_item_status_text_one">%1$d notificació</string>
<string name="channel_subscriber_notification_noinstant_text_more">Subscrit a %1$d temes</string>
<string name="channel_subscriber_notification_noinstant_text_four">Subscrit a quatre temes</string>
<string name="channel_subscriber_notification_noinstant_text_five">Subscrit a cinc temes</string>
<string name="refresh_message_result">%1$d notificacions rebudes</string>
<string name="refresh_message_no_results">Tot està actualitzat</string>
<string name="refresh_message_error">No es poden actualitzar %1$d subscripciones
\n
\n%2$s</string>
<string name="refresh_message_error_one">No s\'ha pogut actualitzar la subscripció: %1$s</string>
<string name="main_action_bar_title">Temes subscrits</string>
<string name="main_menu_notifications_enabled">Notificacions activades</string>
<string name="main_menu_notifications_disabled_forever">Notificacions silenciades</string>
<string name="main_menu_notifications_disabled_until">Notificacions silenciades fins: %1$s</string>
<string name="main_menu_settings_title">Ajustos</string>
<string name="main_menu_report_bug_title">Reportar un problema</string>
<string name="main_menu_docs_title">Llegir la documentació</string>
<string name="main_menu_rate_title">Valora la aplicació ⭐</string>
<string name="main_action_mode_menu_unsubscribe">Donar-se de baixa</string>
<string name="main_action_mode_delete_dialog_message">Donar-se de baixa del tema(es) seleccionat(s) permanentment i eliminar totes les notificacions\?</string>
<string name="main_item_status_text_not_one">%1$d notificacions</string>
</resources>

View file

@ -327,4 +327,6 @@
<string name="main_banner_websocket_button_enable_now">Povolit nyní</string>
<string name="main_banner_websocket_text">WebSockets jsou doporučenou metodou připojení k vašemu serveru, která může zlepšit zvýšit výdrž baterie, ale může vyžadovat <a href="https://ntfy.sh/docs/config/#nginxapache2caddy">další konfiguraci v proxy serveru</a>. Metodu připojení lze přepnout v Nastavení.</string>
<string name="add_dialog_base_urls_dropdown_choose">Zvolit URL služby</string>
<string name="main_menu_donate_title">Přispět 💸</string>
<string name="detail_item_cannot_open_apk">Aplikace již nelze nainstalovat. Místo toho stahujte přes prohlížeč. Podrobnosti naleznete v issue #531.</string>
</resources>

View file

@ -309,7 +309,7 @@
<string name="detail_settings_appearance_header">Darstellung</string>
<string name="detail_settings_appearance_icon_set_title">Abo-Icon</string>
<string name="detail_settings_appearance_icon_set_summary">Ein Icon zur Darstellung in Benachrichtigungen auswählen</string>
<string name="detail_settings_appearance_icon_remove_title">Abo-Icon (entfernen durch antippen)</string>
<string name="detail_settings_appearance_icon_remove_title">Abo-Icon (entfernen durch Antippen)</string>
<string name="detail_settings_appearance_icon_error_saving">Kann Icon nicht speichern: %1$s</string>
<string name="detail_settings_global_setting_title">Globale Einstellung verwenden</string>
<string name="detail_settings_global_setting_suffix">globale Einstellung</string>
@ -323,8 +323,10 @@
<string name="add_dialog_base_urls_dropdown_clear">Service-URL löschen</string>
<string name="detail_settings_appearance_display_name_default_summary">%1$s (Standard)</string>
<string name="detail_settings_appearance_display_name_title">Anzeigename</string>
<string name="detail_settings_appearance_display_name_message">Gib einen eigenen Anzeigenamen für diese Abo an. Leer lassen für den Standardwert (%1$s).</string>
<string name="detail_settings_appearance_display_name_message">Gib einen eigenen Anzeigenamen für dieses Abo an. Leer lassen für den Standardwert (%1$s).</string>
<string name="detail_settings_about_topic_url_title">Themen-URL</string>
<string name="detail_settings_about_header">Über</string>
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">In Zwischenablage kopiert</string>
</resources>
<string name="main_menu_donate_title">Spenden 💸</string>
<string name="detail_item_cannot_open_apk">Apps können nicht mehr installiert werden. Bitte stattdessen über einen Browser herunterladen. Details siehe Issue #531.</string>
</resources>

View file

@ -327,4 +327,6 @@
<string name="add_dialog_base_urls_dropdown_clear">Borrar la URL del servicio</string>
<string name="main_banner_websocket_text">Cambiar a WebSockets es la forma recomendada para conectarse a su servidor, y podría mejorar la vida de la batería, pero puede requerir <a href="https://ntfy.sh/docs/config/#nginxapache2caddy">configuración adicional en su proxy</a>. Esto se puede cambiar en la Configuración.</string>
<string name="main_banner_websocket_button_enable_now">Habilitar ahora</string>
<string name="main_menu_donate_title">Donar 💸</string>
<string name="detail_item_cannot_open_apk">Las aplicaciones ya no se pueden instalar desde ntfy. Descárguelas a través del navegador. Consulte el issue #531 para obtener más información.</string>
</resources>

View file

@ -6,7 +6,7 @@
<string name="channel_notifications_min_name">Notifikasi (prioritas min)</string>
<string name="channel_notifications_low_name">Notifikasi (prioritas rendah)</string>
<string name="channel_subscriber_service_name">Layanan Langganan</string>
<string name="channel_subscriber_notification_title">Mendengarkan untuk notifikasi masuk</string>
<string name="channel_subscriber_notification_title">Mendengarkan notifikasi masuk</string>
<string name="channel_subscriber_notification_instant_text">Berlangganan ke topik pengiriman instan</string>
<string name="channel_subscriber_notification_instant_text_one">Berlangganan ke satu topik pengiriman instan</string>
<string name="channel_subscriber_notification_instant_text_two">Berlangganan ke dua topik pengiriman instan</string>
@ -23,7 +23,7 @@
<string name="main_menu_notifications_disabled_forever">Notifikasi dibisukan</string>
<string name="main_menu_notifications_disabled_until">Notifikasi dibisukan sampai %1$s</string>
<string name="main_menu_settings_title">Pengaturan</string>
<string name="main_menu_report_bug_title">Laporkan sebuah bug</string>
<string name="main_menu_report_bug_title">Laporkan kutu</string>
<string name="main_menu_docs_title">Baca dokumentasi</string>
<string name="main_menu_rate_title">Beri nilai aplikasi ⭐</string>
<string name="main_action_mode_menu_unsubscribe">Batalkan langganan</string>
@ -327,4 +327,6 @@
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">Disalin ke papan klip</string>
<string name="detail_settings_about_header">Tentang</string>
<string name="detail_settings_about_topic_url_title">URL Topik</string>
<string name="main_menu_donate_title">Donasi 💸</string>
<string name="detail_item_cannot_open_apk">Aplikasi tidak dapat dipasang lagi. Unduh melalui peramban. Lihat masalah #531 untuk detail lebih lanjut.</string>
</resources>

View file

@ -15,7 +15,7 @@
<string name="main_action_mode_delete_dialog_permanently_delete">מחק/י לצמיתות</string>
<string name="main_action_mode_delete_dialog_cancel">ביטול</string>
<string name="main_item_status_text_one">התראת %1$d</string>
<string name="main_item_status_text_not_one">התראות %1$d</string>
<string name="main_item_status_text_not_one">%1$d התראות</string>
<string name="main_item_date_yesterday">אתמול</string>
<string name="main_add_button_description">הוספת רישום</string>
<string name="main_no_subscriptions_text">נראה שלא נרשמת לאף נושא עדיין.</string>
@ -39,4 +39,20 @@
<string name="main_item_status_reconnecting">מתחבר מחדש…</string>
<string name="main_how_to_intro">לחצ\\י על + על מנת ליצור או להירשם אל מול נושא מסוים. לאחר מכן תקבל\\י התראות במכשירך כשתשלח\\י התראות דרך PUT או POST.</string>
<string name="main_how_to_link">הוראות מפורטות זמינות ב-ntfy.sh, ובדוקומנטציה.</string>
</resources>
<string name="main_menu_donate_title">תרום 💸</string>
<string name="channel_subscriber_notification_instant_text_two">רשום לשני נושאים במשלוח מהיר</string>
<string name="channel_subscriber_notification_instant_text_three">רשום לשלושה נושאים במשלוח מהיר</string>
<string name="channel_subscriber_notification_instant_text">רשום לנושאים במשלוח מהיר</string>
<string name="channel_subscriber_notification_instant_text_one">רשום לנושא אחד במשלוח מהיר</string>
<string name="channel_subscriber_notification_instant_text_four">רשום לארבעה נושאים במשלוח מהיר</string>
<string name="channel_subscriber_notification_instant_text_five">רשום לחמישה נושאים במשלוח מהיר</string>
<string name="channel_subscriber_notification_instant_text_six">רשום לשישה נושאים במשלוח מהיר</string>
<string name="channel_subscriber_notification_instant_text_more">רשום ל%1$d נושאים במשלוח מהיר</string>
<string name="channel_subscriber_notification_noinstant_text">רשום לנושאים</string>
<string name="channel_subscriber_notification_noinstant_text_one">רשום לנושא אחד</string>
<string name="channel_subscriber_notification_noinstant_text_two">רשום לשני נושאים</string>
<string name="channel_subscriber_notification_noinstant_text_three">רשום לשלושה נושאים</string>
<string name="channel_subscriber_notification_noinstant_text_four">רשום לארבעה נושאים</string>
<string name="channel_subscriber_notification_noinstant_text_five">רשום לחמישה נושאים</string>
<string name="channel_subscriber_notification_noinstant_text_six">רשום לשישה נושאים</string>
</resources>

View file

@ -327,4 +327,6 @@
<string name="detail_settings_about_header">About</string>
<string name="detail_settings_about_topic_url_title">トピックのURL</string>
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">クリップボードにコピーしました</string>
<string name="main_menu_donate_title">寄付する💸</string>
<string name="detail_item_cannot_open_apk">アプリはインストールできなくなりました。代替手段としてブラウザからダウンロードしてください。詳細は issue #531 をご参照ください。</string>
</resources>

View file

@ -77,7 +77,7 @@
<string name="detail_item_tags">Tags: %1$s</string>
<string name="detail_item_snack_deleted">Notificação deletada</string>
<string name="detail_item_snack_undo">Desfazer</string>
<string name="detail_item_menu_download">Fazer download do arquivo</string>
<string name="detail_item_menu_download">Baixar arquivo</string>
<string name="detail_item_menu_cancel">Cancelar o download</string>
<string name="detail_item_cannot_open">Não foi possível abrir o anexo: %1$s</string>
<string name="detail_item_cannot_open_not_found">Não foi possível abrir o anexo: O arquivo pode ter sido deletado, ou não existe app instalado que consiga abrir o arquivo.</string>
@ -258,4 +258,74 @@
<string name="settings_general_dark_mode_summary_light">Modo claro</string>
<string name="settings_general_dark_mode_title">Modo escuro</string>
<string name="settings_general_dark_mode_summary_system">Usar o padrão do sistema</string>
<string name="main_menu_donate_title">Doar 💸</string>
<string name="settings_backup_restore_restore_failed">Recuperação falhou %1$s</string>
<string name="settings_advanced_header">Avançado</string>
<string name="settings_advanced_broadcast_title">Messagens de broadcast</string>
<string name="settings_advanced_broadcast_summary_disabled">Os aplicativos não podem receber notificações por broadcast</string>
<string name="settings_advanced_broadcast_summary_enabled">Os aplicativos já podem receber notificações por broadcast</string>
<string name="settings_advanced_export_logs_entry_copy_original">Copiar para área de transferência</string>
<string name="settings_advanced_export_logs_entry_copy_scrubbed">Copiar para área de transferência (censurado)</string>
<string name="settings_advanced_export_logs_entry_upload_original">Carregar e copiar link</string>
<string name="settings_advanced_export_logs_uploading">Carregando logs …</string>
<string name="settings_advanced_export_logs_copied_url">Logs enviados e URL copiada</string>
<string name="settings_advanced_connection_protocol_summary_jsonhttp">Use um stream de JSON através de HTTP para se conectar ao servidor. Este método foi testado na pratica, mas pode consumir mais bateria.</string>
<string name="settings_about_version_format">ntfy %1$s (%2$s)</string>
<string name="detail_settings_notifications_instant_summary_off">As notificações são entregues utilizando o Firebase. A entrega pode atrasar, mas consome menos bateria.</string>
<string name="detail_settings_appearance_header">Aparência</string>
<string name="detail_settings_appearance_icon_set_title">Ícone de assinatura</string>
<string name="detail_settings_appearance_icon_set_summary">Defina um ícone para ser exibido nas notificações</string>
<string name="detail_settings_appearance_icon_remove_title">Ícone de assinatura (toque para remover)</string>
<string name="detail_settings_appearance_display_name_message">Defina um nome de exibição personalizado para esta assinatura. Deixe em branco para o padrão (%1$s).</string>
<string name="detail_settings_appearance_display_name_default_summary">%1$s (padrão)</string>
<string name="detail_settings_global_setting_title">Usar configuração global</string>
<string name="detail_settings_global_setting_suffix">usar configuração global</string>
<string name="detail_settings_about_header">Sobre</string>
<string name="detail_settings_about_topic_url_title">URL do tópico</string>
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">Copiado para a área de transferência</string>
<string name="user_dialog_title_add">Adicionar usuário</string>
<string name="settings_backup_restore_restore_title">Recuperar do arquivo</string>
<string name="settings_backup_restore_restore_summary">Importar configurações, notificações e usuários</string>
<string name="settings_backup_restore_restore_successful">Restauração concluída</string>
<string name="settings_advanced_record_logs_summary_enabled">Registros (ate 1000 registros) para o dispositivo …</string>
<string name="settings_advanced_record_logs_summary_disabled">Ative o log para que seja possível compartilhar mais tarde os registros para diagnostico.</string>
<string name="settings_advanced_record_logs_title">Logs de registros</string>
<string name="settings_advanced_export_logs_title">Copiar/carregar logs</string>
<string name="settings_advanced_export_logs_summary">Copie os logs para a área de transferência ou faça o upload para nopaste.net (propriedade do autor do ntfy). Hostnames e tópicos podem ser censurados, as notificações não.</string>
<string name="settings_advanced_export_logs_entry_upload_scrubbed">Carregar e copiar link (censurado)</string>
<string name="settings_advanced_export_logs_copied_logs">Logs copiado para área de transferência</string>
<string name="settings_advanced_export_logs_scrub_dialog_text">Esses tópicos/hostnames foram substituídos por nomes de frutas, então você pode compartilhar o log sem se preocupar:
\n
\n%1$s
\n
\nAs senhas são retiradas e não são listadas aqui.</string>
<string name="settings_advanced_export_logs_error_uploading">Não foi possível carregar os logs: %1$s</string>
<string name="settings_advanced_export_logs_scrub_dialog_button_ok">OK</string>
<string name="settings_advanced_export_logs_scrub_dialog_empty">Nenhum tópico/hostname foi editado. Talvez você não tenha nenhuma assinatura\?</string>
<string name="settings_advanced_clear_logs_title">Limpar logs</string>
<string name="settings_advanced_clear_logs_deleted_toast">Logs excluídos</string>
<string name="settings_advanced_connection_protocol_title">Protocolo de conexão</string>
<string name="settings_advanced_clear_logs_summary">Exclua os logs gravados anteriormente e comece de novo</string>
<string name="settings_advanced_connection_protocol_summary_ws">Use WebSockets para se conectar ao servidor. Este é o método recomendado, mas pode exigir configuração adicional em seu proxy.</string>
<string name="settings_advanced_connection_protocol_entry_jsonhttp">Stream JSON através de HTTP</string>
<string name="settings_about_header">Sobre</string>
<string name="settings_about_version_title">Versão</string>
<string name="settings_advanced_connection_protocol_entry_ws">Web Sockets</string>
<string name="detail_settings_notifications_instant_title">Entrega instantânea</string>
<string name="detail_settings_notifications_instant_summary_on">As notificações são entregues instantaneamente. Requer um serviço que roda em primeiro plano e consome mais bateria.</string>
<string name="settings_about_version_copied_to_clipboard_message">Copiado para área de transferência</string>
<string name="detail_settings_appearance_icon_error_saving">Não foi possível salvar o ícone: %1$s</string>
<string name="detail_settings_appearance_icon_remove_summary">Ícone exibido nas notificações deste tópico</string>
<string name="detail_settings_appearance_display_name_title">Nome de exibição</string>
<string name="user_dialog_description_edit">Você pode editar o nome de usuário/senha do usuário selecionado ou excluí-lo.</string>
<string name="user_dialog_base_url_hint">URL do serviço</string>
<string name="user_dialog_title_edit">Editar usuário</string>
<string name="user_dialog_description_add">Você pode adicionar um usuário aqui. Todos os tópicos para o servidor fornecido usarão esse usuário.</string>
<string name="user_dialog_username_hint">Nome do usuário</string>
<string name="user_dialog_password_hint_add">Senha</string>
<string name="user_dialog_password_hint_edit">Senha (se deixada em branco não será alterada)</string>
<string name="user_dialog_button_cancel">Cancelar</string>
<string name="user_dialog_button_add">Adicionar usuário</string>
<string name="user_dialog_button_delete">Deletar usuário</string>
<string name="user_dialog_button_save">Salvar</string>
</resources>

View file

@ -99,4 +99,28 @@
<string name="add_dialog_login_username_hint">Användarnamn</string>
<string name="add_dialog_login_password_hint">Lösenord</string>
<string name="detail_test_message_error_unauthorized_anon">Kan inte skicka meddelande: Anonym publicering är inte tillåten.</string>
<string name="detail_item_snack_deleted">Notifikation borttagen</string>
<string name="detail_item_menu_copy_url_copied">URL kopierad till urklipp</string>
<string name="detail_item_menu_copy_contents">Kopiera notifikation</string>
<string name="detail_item_menu_copy_contents_copied">Notifikation kopierad till urklipp</string>
<string name="detail_item_cannot_open_url">Kan inte öppna URL: %1$s</string>
<string name="detail_menu_clear">Rensa alla notifikationer</string>
<string name="detail_menu_test">Skicka testnotifikation</string>
<string name="detail_action_mode_menu_copy">Kopiera</string>
<string name="detail_action_mode_menu_delete">Ta bort</string>
<string name="detail_action_mode_delete_dialog_permanently_delete">Ta bort permanent</string>
<string name="detail_action_mode_delete_dialog_cancel">Avbryt</string>
<string name="detail_settings_title">Prenumerationsinställningar</string>
<string name="share_title">Dela</string>
<string name="share_menu_send">Dela</string>
<string name="main_menu_donate_title">Donera 💸</string>
<string name="detail_item_snack_undo">Ångra</string>
<string name="detail_item_menu_open">Öppna fil</string>
<string name="detail_item_menu_delete">Ta bort fil</string>
<string name="detail_item_menu_download">Ladda ner fil</string>
<string name="detail_item_menu_cancel">Avbryt nedladdning</string>
<string name="detail_item_menu_save_file">Spara fil</string>
<string name="detail_item_menu_copy_url">Kopiera URL</string>
<string name="detail_item_download_info_download_failed">nedladdning misslyckad</string>
<string name="detail_menu_settings">Prenumerationsinställningar</string>
</resources>

View file

@ -327,4 +327,6 @@
<string name="detail_settings_about_header">Hakkında</string>
<string name="detail_settings_about_topic_url_title">Konu URL\'si</string>
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">Panoya kopyalandı</string>
<string name="main_menu_donate_title">Bağış yap 💸</string>
<string name="detail_item_cannot_open_apk">Uygulamalar artık kurulamıyor. Bunun yerine tarayıcı üzerinden indirin. Ayrıntılar için sorun #531\'e bakın.</string>
</resources>

View file

@ -327,4 +327,6 @@
<string name="detail_settings_about_header">关于</string>
<string name="detail_settings_about_topic_url_title">话题 URL</string>
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">已复制到剪贴板</string>
<string name="main_menu_donate_title">捐赠 💸</string>
<string name="detail_item_cannot_open_apk">无法再安装应用。 请通过浏览器下载。 有关详细信息,请参阅问题 #531。</string>
</resources>

View file

@ -1,23 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="channel_subscriber_notification_noinstant_text_six">已訂閱6個主題</string>
<string name="channel_notifications_default_name">通知(預設優先)</string>
<string name="main_menu_report_bug_title">回報bug</string>
<string name="channel_subscriber_notification_title">監聽傳入通知</string>
<string name="channel_subscriber_notification_noinstant_text_six">已訂閱 6 個主題</string>
<string name="channel_notifications_default_name">通知 (預設優先)</string>
<string name="main_menu_report_bug_title">問題回報</string>
<string name="channel_subscriber_notification_title">正在接收通知</string>
<string name="channel_subscriber_notification_instant_text">已訂閱即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_one">已訂閱1個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_two">已訂閱2個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_four">已訂閱4個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_five">已訂閱5個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_six">已訂閱6個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_more">已訂閱%1$d個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_one">已訂閱 1 個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_two">已訂閱 2 個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_four">已訂閱 4 個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_five">已訂閱 5 個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_six">已訂閱 6 個即時推送主題</string>
<string name="channel_subscriber_notification_instant_text_more">已訂閱 %1$d 個即時推送主題</string>
<string name="channel_subscriber_notification_noinstant_text">已訂閱主題</string>
<string name="refresh_message_result">收到%1$d個通知</string>
<string name="refresh_message_result">收到 %1$d 個通知</string>
<string name="refresh_message_no_results">已同步到最新</string>
<string name="refresh_message_error">有%1$d個訂閱無法更新
<string name="refresh_message_error"> %1$d 個訂閱無法更新
\n
\n%2$s</string>
<string name="refresh_message_error_one">訂閱無法更新:%1$s</string>
<string name="refresh_message_error_one">訂閱無法更新 %1$s</string>
<string name="main_action_bar_title">已訂閱主題</string>
<string name="main_menu_notifications_enabled">通知已開啟</string>
<string name="main_menu_notifications_disabled_forever">通知已靜音</string>
@ -27,8 +27,8 @@
<string name="main_action_mode_delete_dialog_message">取消訂閱已選取的主題且永久刪除所有通知?</string>
<string name="main_action_mode_delete_dialog_permanently_delete">永久刪除</string>
<string name="main_action_mode_delete_dialog_cancel">取消</string>
<string name="main_item_status_text_one">%1$d個通知</string>
<string name="main_item_status_text_not_one">%1$d個通知</string>
<string name="main_item_status_text_one">%1$d 個通知</string>
<string name="main_item_status_text_not_one">%1$d 個通知</string>
<string name="main_item_date_yesterday">昨天</string>
<string name="main_add_button_description">新增訂閱</string>
<string name="main_no_subscriptions_text">看來你還沒有訂閱任何主題。</string>
@ -36,7 +36,7 @@
<string name="main_how_to_link">更多資訊請上 ntfy.shdocs會有更多說明。</string>
<string name="main_unified_push_toast">此訂閱已由 %1$s 透過 UnifiedPush 管理</string>
<string name="main_item_status_unified_push">%1$s (UnifiedPush)</string>
<string name="main_banner_battery_text">為了避免通知傳送問題,電池最佳化必須關閉</string>
<string name="main_banner_battery_text">為了避免通知傳送問題,請務必關閉電池最佳化功能</string>
<string name="main_banner_battery_button_remind_later">稍後詢問我</string>
<string name="main_banner_battery_button_dismiss">略過</string>
<string name="main_banner_battery_button_fix_now">立即修正</string>
@ -47,35 +47,147 @@
<string name="add_dialog_description_below">因為主題不能受密碼保護,請盡量取一個難以猜測的主題名稱。在訂閱之後你就可以使用 PUT/POST 來發送通知。</string>
<string name="add_dialog_use_another_server">使用其他伺服器</string>
<string name="add_dialog_use_another_server_description">在下方輸入自訂的網址來訂閱主題。</string>
<string name="channel_notifications_min_name">通知(最低優先)</string>
<string name="channel_notifications_low_name">通知(低優先)</string>
<string name="channel_notifications_high_name">通知(高優先)</string>
<string name="channel_notifications_max_name">通知(最高優先)</string>
<string name="channel_notifications_min_name">通知 (最低優先)</string>
<string name="channel_notifications_low_name">通知 (低優先)</string>
<string name="channel_notifications_high_name">通知 (高優先)</string>
<string name="channel_notifications_max_name">通知 (最高優先)</string>
<string name="channel_subscriber_service_name">訂閱服務</string>
<string name="channel_subscriber_notification_instant_text_three">已訂閱3個即時推送主題</string>
<string name="main_menu_docs_title">閱讀文件</string>
<string name="main_banner_websocket_text">建議使用 WebSockets 來連線你的伺服器,此動作可以有效增加電池續航,但需要<a href="https://ntfy.sh/docs/config/#nginxapache2caddy">對proxy進行更多設定</a>。這個動作可以在設定中進行</string>
<string name="channel_subscriber_notification_noinstant_text_three">已訂閱3個主題</string>
<string name="channel_subscriber_notification_noinstant_text_more">已訂閱%1$d個主題</string>
<string name="channel_subscriber_notification_noinstant_text_one">已訂閱1個主題</string>
<string name="channel_subscriber_notification_noinstant_text_two">已訂閱2個主題</string>
<string name="channel_subscriber_notification_noinstant_text_four">已訂閱4個主題</string>
<string name="channel_subscriber_notification_noinstant_text_five">已訂閱5個主題</string>
<string name="main_item_status_reconnecting">重新連線中 </string>
<string name="main_menu_notifications_disabled_until">通知靜音到%1$s</string>
<string name="add_dialog_topic_name_hint">主題名稱(例如:phils_alerts)</string>
<string name="channel_subscriber_notification_instant_text_three">已訂閱 3 個即時推送主題</string>
<string name="main_menu_docs_title">閱讀技術文件</string>
<string name="main_banner_websocket_text">建議使用 WebSocket 來接收通知。這個連接方式能有效改善電池續航,但可能需要<a href="https://ntfy.sh/docs/config/#nginxapache2caddy">在伺服器端進行額外設定</a>。你可以在設定頁面更改不同的連接方式</string>
<string name="channel_subscriber_notification_noinstant_text_three">已訂閱 3 個主題</string>
<string name="channel_subscriber_notification_noinstant_text_more">已訂閱 %1$d 個主題</string>
<string name="channel_subscriber_notification_noinstant_text_one">已訂閱 1 個主題</string>
<string name="channel_subscriber_notification_noinstant_text_two">已訂閱 2 個主題</string>
<string name="channel_subscriber_notification_noinstant_text_four">已訂閱 4 個主題</string>
<string name="channel_subscriber_notification_noinstant_text_five">已訂閱 5 個主題</string>
<string name="main_item_status_reconnecting">重新連線中…</string>
<string name="main_menu_notifications_disabled_until">通知靜音到 %1$s</string>
<string name="add_dialog_topic_name_hint">主題名稱 (例如:phils_alerts)</string>
<string name="add_dialog_button_cancel">取消</string>
<string name="add_dialog_button_subscribe">訂閱</string>
<string name="add_dialog_button_back">退</string>
<string name="add_dialog_error_connection_failed">連接失敗: %1$s</string>
<string name="add_dialog_button_back"></string>
<string name="add_dialog_error_connection_failed">連接失敗 %1$s</string>
<string name="add_dialog_login_title">需要登入</string>
<string name="add_dialog_login_username_hint">使用者名</string>
<string name="add_dialog_login_username_hint">使用者名</string>
<string name="add_dialog_login_password_hint">密碼</string>
<string name="add_dialog_login_error_not_authorized">登入失敗. 使用者%1$s 未獲得授權.</string>
<string name="add_dialog_login_new_user">新使用者</string>
<string name="add_dialog_base_urls_dropdown_choose">選擇服務網址</string>
<string name="add_dialog_base_urls_dropdown_clear">清除服務網址</string>
<string name="detail_copied_to_clipboard_message">複製到剪貼簿</string>
<string name="add_dialog_login_error_not_authorized">登入失敗,使用者 %1$s 並未授權訂閱這個主題。</string>
<string name="add_dialog_login_new_user">建立新使用者</string>
<string name="add_dialog_base_urls_dropdown_choose">選擇 ntfy 服務 URL</string>
<string name="add_dialog_base_urls_dropdown_clear">清除 URL</string>
<string name="detail_copied_to_clipboard_message">複製到剪貼簿</string>
<string name="add_dialog_button_login">登入</string>
<string name="add_dialog_login_description">這篇主題要求登入. 請輸入帳號密碼.</string>
<string name="add_dialog_login_description">這個主題需要登入,請先輸入帳號密碼。</string>
<string name="add_dialog_instant_delivery">在省電模式下依然接收即時通知</string>
<string name="add_dialog_instant_delivery_description">確保通知能在未使用裝置時都能接收。</string>
<string name="detail_clear_dialog_cancel">取消</string>
<string name="detail_delete_dialog_cancel">取消</string>
<string name="detail_action_mode_delete_dialog_cancel">取消</string>
<string name="share_title">分享</string>
<string name="share_topic_title">分享到</string>
<string name="settings_notifications_priority_low"></string>
<string name="settings_notifications_priority_default">預設</string>
<string name="settings_notifications_priority_high"></string>
<string name="settings_notifications_priority_max">最高</string>
<string name="settings_notifications_auto_delete_summary_one_month">自動刪除 1 個月前的通知</string>
<string name="settings_notifications_auto_delete_one_day">1 天後</string>
<string name="settings_notifications_auto_delete_three_days">3 天後</string>
<string name="settings_notifications_auto_delete_one_week">1 週後</string>
<string name="settings_notifications_auto_delete_one_month">1 個月後</string>
<string name="settings_notifications_auto_delete_three_months">3 個月後</string>
<string name="settings_general_header">一般</string>
<string name="settings_general_default_base_url_title">預設伺服器</string>
<string name="settings_general_default_base_url_default_summary">%1$s (預設)</string>
<string name="settings_general_users_title">管理使用者</string>
<string name="settings_backup_restore_header">備份與還原</string>
<string name="settings_about_version_copied_to_clipboard_message">已複製到剪貼簿</string>
<string name="settings_about_header">關於</string>
<string name="settings_about_version_title">版本號</string>
<string name="settings_about_version_format">ntfy %1$s (%2$s)</string>
<string name="detail_settings_appearance_header">主題</string>
<string name="detail_settings_notifications_instant_title">即時通知</string>
<string name="detail_settings_global_setting_suffix">使用全域設定</string>
<string name="user_dialog_title_add">新增使用者</string>
<string name="main_menu_donate_title">捐獻 💸</string>
<string name="detail_item_snack_undo">復原</string>
<string name="detail_item_download_info_downloading_x_percent">已下載 %1$d%%</string>
<string name="detail_menu_enable_instant">啓用即時通知</string>
<string name="detail_menu_disable_instant">關閉即時通知</string>
<string name="detail_menu_clear">清除所有通知</string>
<string name="detail_action_mode_menu_copy">複製</string>
<string name="detail_action_mode_menu_delete">刪除</string>
<string name="share_menu_send">傳送</string>
<string name="share_content_title">預覽</string>
<string name="notification_dialog_cancel">取消</string>
<string name="notification_dialog_save">儲存</string>
<string name="notification_dialog_30min">30 分鐘</string>
<string name="notification_dialog_1h">1 小時</string>
<string name="notification_dialog_2h">2 小時</string>
<string name="notification_dialog_8h">8 小時</string>
<string name="notification_dialog_tomorrow">直到明天</string>
<string name="notification_dialog_forever">直到我解除為止</string>
<string name="notification_popup_action_open">開啓</string>
<string name="notification_popup_action_browse">瀏覽</string>
<string name="notification_popup_action_download">下載</string>
<string name="notification_popup_action_cancel">取消</string>
<string name="notification_popup_file">%1$s
\n檔案 %2$s</string>
<string name="notification_popup_file_downloading">下載中 %1$s, %2$d%%
\n%3$s</string>
<string name="notification_popup_file_download_successful">%1$s
\n檔案下載已完成 %2$s</string>
<string name="notification_popup_file_download_failed">%1$s
\n檔案下載失敗 %2$s</string>
<string name="notification_popup_user_action_failed">%1$s 失敗: %2$s</string>
<string name="settings_title">設定</string>
<string name="settings_notifications_muted_until_show_all">顯示所有通知</string>
<string name="settings_notifications_priority_min">分鐘</string>
<string name="settings_notifications_auto_download_title">下載附件</string>
<string name="settings_notifications_auto_download_summary_always">自動下載所有附件</string>
<string name="settings_notifications_auto_download_summary_never">從不自動下載附件</string>
<string name="settings_notifications_auto_download_summary_smaller_than_x">自動下載小於 %1$s 的附件</string>
<string name="settings_notifications_auto_download_never">從不自動下載</string>
<string name="settings_notifications_auto_download_always">自動下載所有東西</string>
<string name="settings_notifications_auto_download_500k">小於 500 kB</string>
<string name="settings_notifications_auto_download_100k">小於 100 kB</string>
<string name="settings_notifications_auto_download_1m">小於 1 MB</string>
<string name="settings_notifications_auto_download_5m">小於 5 MB</string>
<string name="settings_notifications_auto_download_50m">小於 50 MB</string>
<string name="settings_notifications_auto_delete_title">自動刪除通知</string>
<string name="settings_notifications_auto_delete_summary_never">從不自動刪除通知</string>
<string name="settings_notifications_auto_delete_summary_one_day">自動刪除 1 天前的通知</string>
<string name="settings_notifications_auto_delete_summary_three_days">自動刪除 3 天前的通知</string>
<string name="settings_notifications_auto_delete_summary_one_week">自動刪除 1 週前的通知</string>
<string name="settings_notifications_auto_delete_summary_three_months">自動刪除 3 個月前的通知</string>
<string name="settings_notifications_auto_delete_never">從不</string>
<string name="settings_general_users_prefs_title">使用者</string>
<string name="settings_general_users_prefs_user_add">新增使用者</string>
<string name="settings_general_users_prefs_user_add_title">建立新使用者</string>
<string name="settings_general_dark_mode_title">黑暗模式</string>
<string name="settings_backup_restore_backup_entry_everything">全部</string>
<string name="settings_backup_restore_backup_entry_settings_only">僅備份設定</string>
<string name="settings_backup_restore_backup_entry_everything_no_users">全部備份 (使用者檔案除外)</string>
<string name="settings_backup_restore_backup_successful">已成功備份</string>
<string name="settings_backup_restore_restore_successful">還原成功</string>
<string name="settings_backup_restore_restore_failed">還原失敗: %1$s</string>
<string name="settings_advanced_header">進階</string>
<string name="settings_advanced_export_logs_scrub_dialog_button_ok">確定</string>
<string name="settings_advanced_connection_protocol_title">連接方式</string>
<string name="settings_advanced_connection_protocol_summary_jsonhttp">透過 HTTP 連接伺服器來接收 JSON 串流,但可能會持續消耗較大電量。</string>
<string name="settings_advanced_connection_protocol_summary_ws">(建議) 透過 WebSocket 連接伺服器,但伺服器可能需要額外設定來啓用這種連接方式。</string>
<string name="settings_advanced_connection_protocol_entry_jsonhttp">JSON 串流 (透過 HTTP 連接)</string>
<string name="settings_advanced_connection_protocol_entry_ws">WebSocket</string>
<string name="detail_settings_about_topic_url_title">主題 URL</string>
<string name="detail_settings_about_topic_url_copied_to_clipboard_message">已複製到剪貼簿</string>
<string name="user_dialog_password_hint_add">密碼</string>
<string name="user_dialog_button_save">儲存</string>
<string name="user_dialog_button_add">新增使用者</string>
<string name="user_dialog_button_cancel">取消</string>
<string name="user_dialog_username_hint">使用者名稱</string>
<string name="settings_notifications_header">通知</string>
<string name="detail_menu_test">發送測試訊息</string>
<string name="settings_notifications_muted_until_title">暫停通知</string>
<string name="settings_notifications_auto_download_10m">小於 10 MB</string>
<string name="detail_settings_about_header">關於</string>
<string name="settings_backup_restore_backup_failed">備份失敗: %1$s</string>
</resources>

View file

@ -154,6 +154,7 @@
<string name="detail_item_cannot_open">Cannot open attachment: %1$s</string>
<string name="detail_item_cannot_open_not_found">Cannot open attachment: The file may have been deleted, or no installed app can open the file.</string>
<string name="detail_item_cannot_open_url">Cannot open URL: %1$s</string>
<string name="detail_item_cannot_open_apk">Apps cannot be installed anymore. Download via browser instead. See issue #531 for details.</string>
<string name="detail_item_cannot_save">Cannot save attachment: %1$s</string>
<string name="detail_item_cannot_delete">Cannot delete attachment: %1$s</string>
<string name="detail_item_download_failed">Could not download attachment: %1$s</string>
@ -318,7 +319,7 @@
<string name="settings_advanced_broadcast_summary_disabled">Apps cannot receive notifications as broadcasts</string>
<string name="settings_advanced_record_logs_title">Record logs</string>
<string name="settings_advanced_record_logs_summary_enabled">Logging (up to 1,000 entries) to device …</string>
<string name="settings_advanced_record_logs_summary_disabled">Turn on logging so you can share logs later to diagnose issues.</string>
<string name="settings_advanced_record_logs_summary_disabled">Turn on logging, so you can share logs later to diagnose issues.</string>
<string name="settings_advanced_export_logs_title">Copy/upload logs</string>
<string name="settings_advanced_export_logs_summary">Copy logs to the clipboard, or upload to nopaste.net (owned by the ntfy author). Hostnames and topics can be censored, notifications will never be.</string>
<string name="settings_advanced_export_logs_entry_copy_original">Copy to clipboard</string>

View file

@ -14,6 +14,7 @@ import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationDispatcher
import io.heckel.ntfy.msg.NotificationParser
import io.heckel.ntfy.service.SubscriberService
import io.heckel.ntfy.util.nullIfZero
import io.heckel.ntfy.util.toPriority
import io.heckel.ntfy.util.topicShortUrl
import io.heckel.ntfy.work.PollWorker
@ -94,8 +95,8 @@ class FirebaseService : FirebaseMessagingService() {
val encoding = data["encoding"]
val attachmentName = data["attachment_name"] ?: "attachment.bin"
val attachmentType = data["attachment_type"]
val attachmentSize = data["attachment_size"]?.toLongOrNull()
val attachmentExpires = data["attachment_expires"]?.toLongOrNull()
val attachmentSize = data["attachment_size"]?.toLongOrNull()?.nullIfZero()
val attachmentExpires = data["attachment_expires"]?.toLongOrNull()?.nullIfZero()
val attachmentUrl = data["attachment_url"]
val truncated = (data["truncated"] ?: "") == "1"
if (id == null || topic == null || message == null || timestamp == null) {

View file

@ -12,6 +12,8 @@ Bug fixes + maintenance:
* Fix topics do not re-subscribe to Firebase after restoring from backup (#511)
* Fix crashes from large images (#474, thanks to @daedric7 for reporting)
* Fix notification click opens wrong subscription (#261, thanks to @SMAW for reporting)
* Fix Firebase-only "link expired" issue (#529)
* Remove "Install .apk" feature in Google Play variant due to policy change (#531)
* Add donate button (no ticket)
Additional translations:

View file

@ -0,0 +1,2 @@
Bug fixes:
* Android 5 (SDK 21): Fix crash on unsubscribing (#528, thanks to Roger M.)

View file

@ -0,0 +1,17 @@
不論 Bash 還是 PowerShell或者是你自己的應用程式、curl 或者 Invoke-WebRequest 等等,都可以透過 HTTP PUT/POST 向你的裝置傳送通知。
ntfy 是 https://ntfy.sh 的 Android APP一個打造在 HTTP 標準之上的免費開源 pub-sub 服務。你可以透過訂閱主題來接收通過 HTTP API 發送的通知。
當中,你可能找到千變萬化的用途,例如:
* 在一個很長很長的程序完成後通知自己
* 在備份失敗後通知自己
* 當有人登入到伺服器的時候發送通知
發送通知可以簡單如此:
$ curl -d "備份完成了!" ntfy.sh/mytopic
你也可以在下面的連結閱讀更多資訊:
* 網站: https://ntfy.sh
* GitHub伺服器端 https://github.com/binwiederhier/ntfy
* GitHubAndroid APP https://github.com/binwiederhier/ntfy-android

View file

@ -0,0 +1 @@
透過 HTTP PUT/POST 方式傳送任何通知到你的裝置

View file

@ -0,0 +1 @@
ntfy - PUT/POST 到你的裝置