Upload log feature
This commit is contained in:
parent
e402bbf381
commit
4855ba7d32
5 changed files with 191 additions and 86 deletions
|
@ -1,12 +1,16 @@
|
||||||
package io.heckel.ntfy.log
|
package io.heckel.ntfy.log
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import io.heckel.ntfy.BuildConfig
|
||||||
import io.heckel.ntfy.db.Database
|
import io.heckel.ntfy.db.Database
|
||||||
import io.heckel.ntfy.db.LogDao
|
import io.heckel.ntfy.db.LogDao
|
||||||
import io.heckel.ntfy.db.LogEntry
|
import io.heckel.ntfy.db.LogEntry
|
||||||
|
import io.heckel.ntfy.util.isIgnoringBatteryOptimizations
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
@ -29,9 +33,40 @@ class Log(private val logsDao: LogDao) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAll(): Collection<LogEntry> {
|
fun getFormatted(): String {
|
||||||
return logsDao
|
return prependDeviceInfo(formatEntries(scrubEntries(logsDao.getAll())))
|
||||||
.getAll()
|
}
|
||||||
|
|
||||||
|
private fun prependDeviceInfo(s: String): String {
|
||||||
|
return """
|
||||||
|
This is a log of the ntfy Android app. The log shows up to 5,000 lines.
|
||||||
|
Server URLs (aside from ntfy.sh) and topics have been replaced with fruits 🍌🥝🍋🥥🥑🍊🍎🍑.
|
||||||
|
|
||||||
|
Device info:
|
||||||
|
--
|
||||||
|
ntfy: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR})
|
||||||
|
OS: ${System.getProperty("os.version")}
|
||||||
|
Android: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})
|
||||||
|
Model: ${Build.DEVICE}
|
||||||
|
Product: ${Build.PRODUCT}
|
||||||
|
---
|
||||||
|
""".trimIndent() + "\n\n$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addScrubTerm(term: String, type: TermType = TermType.Term) {
|
||||||
|
if (scrubTerms[term] != null || IGNORE_TERMS.contains(term)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val replaceTermIndex = scrubNum.incrementAndGet()
|
||||||
|
val replaceTerm = REPLACE_TERMS.getOrNull(replaceTermIndex) ?: "fruit${replaceTermIndex}"
|
||||||
|
scrubTerms[term] = when (type) {
|
||||||
|
TermType.Domain -> "$replaceTerm.example.com"
|
||||||
|
else -> replaceTerm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scrubEntries(entries: List<LogEntry>): List<LogEntry> {
|
||||||
|
return entries
|
||||||
.map { e ->
|
.map { e ->
|
||||||
e.copy(
|
e.copy(
|
||||||
message = scrub(e.message)!!,
|
message = scrub(e.message)!!,
|
||||||
|
@ -40,22 +75,6 @@ class Log(private val logsDao: LogDao) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteAll() {
|
|
||||||
return logsDao.deleteAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addScrubTerm(term: String, type: TermType = TermType.Term) {
|
|
||||||
if (scrubTerms[term] != null || IGNORE_TERMS.contains(term)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val replaceTermIndex = scrubNum.incrementAndGet()
|
|
||||||
val replaceTerm = REPLACE_TERMS.getOrNull(replaceTermIndex) ?: "scrubbed${replaceTermIndex}"
|
|
||||||
scrubTerms[term] = when (type) {
|
|
||||||
TermType.Domain -> "$replaceTerm.example.com"
|
|
||||||
else -> replaceTerm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scrub(line: String?): String? {
|
private fun scrub(line: String?): String? {
|
||||||
var newLine = line ?: return null
|
var newLine = line ?: return null
|
||||||
scrubTerms.forEach { (scrubTerm, replaceTerm) ->
|
scrubTerms.forEach { (scrubTerm, replaceTerm) ->
|
||||||
|
@ -64,6 +83,31 @@ class Log(private val logsDao: LogDao) {
|
||||||
return newLine
|
return newLine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun formatEntries(entries: List<LogEntry>): String {
|
||||||
|
return entries.joinToString(separator = "\n") { e ->
|
||||||
|
val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(Date(e.timestamp))
|
||||||
|
val level = when (e.level) {
|
||||||
|
android.util.Log.DEBUG -> "D"
|
||||||
|
android.util.Log.INFO -> "I"
|
||||||
|
android.util.Log.WARN -> "W"
|
||||||
|
android.util.Log.ERROR -> "E"
|
||||||
|
else -> "?"
|
||||||
|
}
|
||||||
|
val tag = e.tag.format("%-23s")
|
||||||
|
val prefix = "${e.timestamp} $date $level $tag"
|
||||||
|
val message = if (e.exception != null) {
|
||||||
|
"${e.message}\nException:\n${e.exception}"
|
||||||
|
} else {
|
||||||
|
e.message
|
||||||
|
}
|
||||||
|
"$prefix $message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteAll() {
|
||||||
|
return logsDao.deleteAll()
|
||||||
|
}
|
||||||
|
|
||||||
enum class TermType {
|
enum class TermType {
|
||||||
Domain, Term
|
Domain, Term
|
||||||
}
|
}
|
||||||
|
@ -74,7 +118,7 @@ class Log(private val logsDao: LogDao) {
|
||||||
private const val ENTRIES_MAX = 5000
|
private const val ENTRIES_MAX = 5000
|
||||||
private val IGNORE_TERMS = listOf("ntfy.sh")
|
private val IGNORE_TERMS = listOf("ntfy.sh")
|
||||||
private val REPLACE_TERMS = listOf(
|
private val REPLACE_TERMS = listOf(
|
||||||
"potato", "banana", "coconut", "kiwi", "avocado", "orange", "apple", "lemon", "olive", "peach"
|
"banana", "kiwi", "lemon", "coconut", "avocado", "orange", "apple", "peach"
|
||||||
)
|
)
|
||||||
private var instance: Log? = null
|
private var instance: Log? = null
|
||||||
|
|
||||||
|
@ -108,12 +152,13 @@ class Log(private val logsDao: LogDao) {
|
||||||
return getInstance()?.record?.get() ?: false
|
return getInstance()?.record?.get() ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAll(): Collection<LogEntry> {
|
fun getFormatted(): String {
|
||||||
return getInstance()?.getAll().orEmpty()
|
return getInstance()?.getFormatted() ?: "(no logs)"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
getInstance()?.deleteAll()
|
getInstance()?.deleteAll()
|
||||||
|
d(TAG, "Log was truncated")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addScrubTerm(term: String, type: TermType = TermType.Term) {
|
fun addScrubTerm(term: String, type: TermType = TermType.Term) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package io.heckel.ntfy.ui
|
package io.heckel.ntfy.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -11,6 +10,7 @@ import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.Keep
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
@ -18,6 +18,7 @@ import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.*
|
import androidx.preference.*
|
||||||
import androidx.preference.Preference.OnPreferenceClickListener
|
import androidx.preference.Preference.OnPreferenceClickListener
|
||||||
|
import com.google.gson.Gson
|
||||||
import io.heckel.ntfy.BuildConfig
|
import io.heckel.ntfy.BuildConfig
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
|
@ -28,8 +29,10 @@ import io.heckel.ntfy.util.formatDateShort
|
||||||
import io.heckel.ntfy.util.toPriorityString
|
import io.heckel.ntfy.util.toPriorityString
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.text.SimpleDateFormat
|
import okhttp3.OkHttpClient
|
||||||
import java.util.*
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
private lateinit var fragment: SettingsFragment
|
private lateinit var fragment: SettingsFragment
|
||||||
|
@ -222,14 +225,26 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy logs
|
// Export logs
|
||||||
val copyLogsPrefId = context?.getString(R.string.settings_advanced_copy_logs_key) ?: return
|
val exportLogsPrefId = context?.getString(R.string.settings_advanced_export_logs_key) ?: return
|
||||||
val copyLogs: Preference? = findPreference(copyLogsPrefId)
|
val exportLogs: ListPreference? = findPreference(exportLogsPrefId)
|
||||||
copyLogs?.isVisible = Log.getRecord()
|
exportLogs?.isVisible = Log.getRecord()
|
||||||
copyLogs?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
|
exportLogs?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
|
||||||
copyLogs?.onPreferenceClickListener = OnPreferenceClickListener {
|
exportLogs?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, v ->
|
||||||
copyLogsToClipboard()
|
when (v) {
|
||||||
true
|
EXPORT_LOGS_COPY -> copyLogsToClipboard()
|
||||||
|
EXPORT_LOGS_UPLOAD -> uploadLogsToNopaste()
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
val clearLogsPrefId = context?.getString(R.string.settings_advanced_clear_logs_key) ?: return
|
||||||
|
val clearLogs: Preference? = findPreference(clearLogsPrefId)
|
||||||
|
clearLogs?.isVisible = Log.getRecord()
|
||||||
|
clearLogs?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
|
||||||
|
clearLogs?.onPreferenceClickListener = OnPreferenceClickListener {
|
||||||
|
deleteLogs()
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record logs
|
// Record logs
|
||||||
|
@ -240,7 +255,8 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
override fun putBoolean(key: String?, value: Boolean) {
|
override fun putBoolean(key: String?, value: Boolean) {
|
||||||
repository.setRecordLogsEnabled(value)
|
repository.setRecordLogsEnabled(value)
|
||||||
Log.setRecord(value)
|
Log.setRecord(value)
|
||||||
copyLogs?.isVisible = value
|
exportLogs?.isVisible = value
|
||||||
|
clearLogs?.isVisible = value
|
||||||
}
|
}
|
||||||
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||||
return Log.getRecord()
|
return Log.getRecord()
|
||||||
|
@ -253,29 +269,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
getString(R.string.settings_advanced_record_logs_summary_disabled)
|
getString(R.string.settings_advanced_record_logs_summary_disabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
recordLogsEnabled?.setOnPreferenceChangeListener { _, v ->
|
|
||||||
val newValue = v as Boolean
|
|
||||||
if (!newValue) {
|
|
||||||
val dialog = AlertDialog.Builder(activity)
|
|
||||||
.setMessage(R.string.settings_advanced_record_logs_delete_dialog_message)
|
|
||||||
.setPositiveButton(R.string.settings_advanced_record_logs_delete_dialog_button_delete) { _, _ ->
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
Log.deleteAll()
|
|
||||||
} }
|
|
||||||
.setNegativeButton(R.string.settings_advanced_record_logs_delete_dialog_button_keep) { _, _ ->
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
.create()
|
|
||||||
dialog
|
|
||||||
.setOnShowListener {
|
|
||||||
dialog
|
|
||||||
.getButton(AlertDialog.BUTTON_POSITIVE)
|
|
||||||
.setTextColor(ContextCompat.getColor(requireContext(), R.color.primaryDangerButtonColor))
|
|
||||||
}
|
|
||||||
dialog.show()
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connection protocol
|
// Connection protocol
|
||||||
val connectionProtocolPrefId = context?.getString(R.string.settings_advanced_connection_protocol_key) ?: return
|
val connectionProtocolPrefId = context?.getString(R.string.settings_advanced_connection_protocol_key) ?: return
|
||||||
|
@ -354,35 +347,79 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private fun copyLogsToClipboard() {
|
private fun copyLogsToClipboard() {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val log = Log.getAll().joinToString(separator = "\n") { e ->
|
val log = Log.getFormatted()
|
||||||
val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(Date(e.timestamp))
|
|
||||||
val level = when (e.level) {
|
|
||||||
android.util.Log.DEBUG -> "D"
|
|
||||||
android.util.Log.INFO -> "I"
|
|
||||||
android.util.Log.WARN -> "W"
|
|
||||||
android.util.Log.ERROR -> "E"
|
|
||||||
else -> "?"
|
|
||||||
}
|
|
||||||
val tag = e.tag.format("%-23s")
|
|
||||||
val prefix = "${e.timestamp} $date $level $tag"
|
|
||||||
val message = if (e.exception != null) {
|
|
||||||
"${e.message}\nException:\n${e.exception}"
|
|
||||||
} else {
|
|
||||||
e.message
|
|
||||||
}
|
|
||||||
"$prefix $message"
|
|
||||||
}
|
|
||||||
val context = context ?: return@launch
|
val context = context ?: return@launch
|
||||||
requireActivity().runOnUiThread {
|
requireActivity().runOnUiThread {
|
||||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
val clip = ClipData.newPlainText("ntfy logs", log)
|
val clip = ClipData.newPlainText("ntfy logs", log)
|
||||||
clipboard.setPrimaryClip(clip)
|
clipboard.setPrimaryClip(clip)
|
||||||
Toast
|
Toast
|
||||||
.makeText(context, getString(R.string.settings_advanced_copy_logs_copied), Toast.LENGTH_LONG)
|
.makeText(context, getString(R.string.settings_advanced_export_logs_copied_logs), Toast.LENGTH_LONG)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun uploadLogsToNopaste() {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
Log.d(TAG, "Uploading log to $EXPORT_LOGS_UPLOAD_URL ...")
|
||||||
|
val log = Log.getFormatted()
|
||||||
|
val gson = Gson()
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(EXPORT_LOGS_UPLOAD_URL)
|
||||||
|
.put(log.toRequestBody())
|
||||||
|
.build()
|
||||||
|
val client = OkHttpClient.Builder()
|
||||||
|
.callTimeout(1, TimeUnit.MINUTES) // Total timeout for entire request
|
||||||
|
.connectTimeout(15, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(15, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(15, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
try {
|
||||||
|
client.newCall(request).execute().use { response ->
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw Exception("Unexpected response ${response.code}")
|
||||||
|
}
|
||||||
|
val body = response.body?.string()?.trim()
|
||||||
|
if (body == null || body.isEmpty()) throw Exception("Return body is empty")
|
||||||
|
Log.d(TAG, "Logs uploaded successfully: $body")
|
||||||
|
val resp = gson.fromJson(body.toString(), NopasteResponse::class.java)
|
||||||
|
val context = context ?: return@launch
|
||||||
|
requireActivity().runOnUiThread {
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText("logs URL", resp.url)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
Toast
|
||||||
|
.makeText(context, getString(R.string.settings_advanced_export_logs_copied_url), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error uploading logs", e)
|
||||||
|
val context = context ?: return@launch
|
||||||
|
requireActivity().runOnUiThread {
|
||||||
|
Toast
|
||||||
|
.makeText(context, getString(R.string.settings_advanced_export_logs_error_uploading, e.message), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteLogs() {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
Log.deleteAll()
|
||||||
|
val context = context ?: return@launch
|
||||||
|
requireActivity().runOnUiThread {
|
||||||
|
Toast
|
||||||
|
.makeText(context, getString(R.string.settings_advanced_clear_logs_deleted_toast), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
data class NopasteResponse(val url: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
|
@ -403,5 +440,8 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
private const val TAG = "NtfySettingsActivity"
|
private const val TAG = "NtfySettingsActivity"
|
||||||
private const val REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD = 2586
|
private const val REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD = 2586
|
||||||
private const val AUTO_DOWNLOAD_SELECTION_NOT_SET = -99L
|
private const val AUTO_DOWNLOAD_SELECTION_NOT_SET = -99L
|
||||||
|
private const val EXPORT_LOGS_COPY = "copy"
|
||||||
|
private const val EXPORT_LOGS_UPLOAD = "upload"
|
||||||
|
private const val EXPORT_LOGS_UPLOAD_URL = "https://nopaste.net/?f=json" // Run by binwiederhier; see https://github.com/binwiederhier/pcopy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -240,13 +240,18 @@
|
||||||
<string name="settings_advanced_record_logs_title">Record logs</string>
|
<string name="settings_advanced_record_logs_title">Record logs</string>
|
||||||
<string name="settings_advanced_record_logs_summary_enabled">Logs are currently being recorded to your device. Up to 5,000 log lines are stored.</string>
|
<string name="settings_advanced_record_logs_summary_enabled">Logs are currently being recorded to your device. Up to 5,000 log lines are stored.</string>
|
||||||
<string name="settings_advanced_record_logs_summary_disabled">Enable log recording, so you can share the logs later. This is useful for diagnosing issues.</string>
|
<string name="settings_advanced_record_logs_summary_disabled">Enable log recording, so you can share the logs later. This is useful for diagnosing issues.</string>
|
||||||
<string name="settings_advanced_record_logs_delete_dialog_message">Would you like to delete the existing logs?</string>
|
<string name="settings_advanced_export_logs_key">ExportLogs</string>
|
||||||
<string name="settings_advanced_record_logs_delete_dialog_button_keep">Keep logs</string>
|
<string name="settings_advanced_export_logs_title">Copy/upload logs</string>
|
||||||
<string name="settings_advanced_record_logs_delete_dialog_button_delete">Delete logs</string>
|
<string name="settings_advanced_export_logs_summary">Copy logs to the clipboard, or upload to nopaste.net (owned by ntfy author). Hostnames and topics are scrubbed, notifications are not.</string>
|
||||||
<string name="settings_advanced_copy_logs_key">CopyLogs</string>
|
<string name="settings_advanced_export_logs_entry_copy">Copy to clipboard</string>
|
||||||
<string name="settings_advanced_copy_logs_title">Copy logs</string>
|
<string name="settings_advanced_export_logs_entry_upload">Upload to nopaste.net</string>
|
||||||
<string name="settings_advanced_copy_logs_summary">Copy logs to the clipboard. Hostnames and topics are scrubbed, notifications are not.</string>
|
<string name="settings_advanced_export_logs_copied_logs">Logs copied to clipboard</string>
|
||||||
<string name="settings_advanced_copy_logs_copied">Copied to clipboard</string>
|
<string name="settings_advanced_export_logs_copied_url">URL copied to clipboard</string>
|
||||||
|
<string name="settings_advanced_export_logs_error_uploading">Error uploading logs: %1$s</string>
|
||||||
|
<string name="settings_advanced_clear_logs_key">ClearLogs</string>
|
||||||
|
<string name="settings_advanced_clear_logs_title">Clear logs</string>
|
||||||
|
<string name="settings_advanced_clear_logs_summary">Delete previously recorded logs, and start over</string>
|
||||||
|
<string name="settings_advanced_clear_logs_deleted_toast">Logs successfully deleted</string>
|
||||||
<string name="settings_experimental_header">Experimental</string>
|
<string name="settings_experimental_header">Experimental</string>
|
||||||
<string name="settings_advanced_connection_protocol_key">ConnectionProtocol</string>
|
<string name="settings_advanced_connection_protocol_key">ConnectionProtocol</string>
|
||||||
<string name="settings_advanced_connection_protocol_title">Connection protocol</string>
|
<string name="settings_advanced_connection_protocol_title">Connection protocol</string>
|
||||||
|
|
|
@ -42,4 +42,12 @@
|
||||||
<item>jsonhttp</item>
|
<item>jsonhttp</item>
|
||||||
<item>ws</item>
|
<item>ws</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="settings_advanced_export_logs_entries">
|
||||||
|
<item>@string/settings_advanced_export_logs_entry_copy</item>
|
||||||
|
<item>@string/settings_advanced_export_logs_entry_upload</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="settings_advanced_export_logs_values">
|
||||||
|
<item>copy</item>
|
||||||
|
<item>upload</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -42,10 +42,17 @@
|
||||||
app:key="@string/settings_advanced_record_logs_key"
|
app:key="@string/settings_advanced_record_logs_key"
|
||||||
app:title="@string/settings_advanced_record_logs_title"
|
app:title="@string/settings_advanced_record_logs_title"
|
||||||
app:enabled="true"/>
|
app:enabled="true"/>
|
||||||
|
<ListPreference
|
||||||
|
app:key="@string/settings_advanced_export_logs_key"
|
||||||
|
app:title="@string/settings_advanced_export_logs_title"
|
||||||
|
app:summary="@string/settings_advanced_export_logs_summary"
|
||||||
|
app:entries="@array/settings_advanced_export_logs_entries"
|
||||||
|
app:entryValues="@array/settings_advanced_export_logs_values"
|
||||||
|
app:defaultValue="copy"/>
|
||||||
<Preference
|
<Preference
|
||||||
app:key="@string/settings_advanced_copy_logs_key"
|
app:key="@string/settings_advanced_clear_logs_key"
|
||||||
app:title="@string/settings_advanced_copy_logs_title"
|
app:title="@string/settings_advanced_clear_logs_title"
|
||||||
android:summary="@string/settings_advanced_copy_logs_summary"/>
|
app:summary="@string/settings_advanced_clear_logs_summary"/>
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory app:title="@string/settings_experimental_header">
|
<PreferenceCategory app:title="@string/settings_experimental_header">
|
||||||
<ListPreference
|
<ListPreference
|
||||||
|
|
Loading…
Reference in a new issue