Export notification logs through document picker

This commit is contained in:
ajp_anton
2026-07-23 09:50:14 +00:00
parent 4345ecbf2f
commit 4b26f3369e
2 changed files with 37 additions and 1 deletions
@@ -3,6 +3,7 @@ package se.ajpanton.notificationlog
import android.os.Bundle import android.os.Bundle
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.provider.Settings import android.provider.Settings
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
@@ -15,11 +16,17 @@ import se.ajpanton.notificationlog.settings.LogField
import se.ajpanton.notificationlog.settings.LogViewSettingsStore import se.ajpanton.notificationlog.settings.LogViewSettingsStore
import se.ajpanton.notificationlog.settings.TimestampZone import se.ajpanton.notificationlog.settings.TimestampZone
import se.ajpanton.notificationlog.capture.NotificationCaptureService import se.ajpanton.notificationlog.capture.NotificationCaptureService
import se.ajpanton.notificationlog.export.LogExporter
import java.util.zip.ZipOutputStream
import java.text.DateFormat import java.text.DateFormat
import java.util.Date import java.util.Date
class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) { class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
private var binding: FragmentViewLogsBinding? = null private var binding: FragmentViewLogsBinding? = null
private var pendingExport: ExportFormat? = null
private val createDocument = registerForActivityResult(androidx.activity.result.contract.ActivityResultContracts.CreateDocument("application/octet-stream")) { uri ->
uri?.let(::writeExport)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -27,6 +34,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
binding!!.notificationAccess.setOnClickListener { binding!!.notificationAccess.setOnClickListener {
startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)) startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
} }
binding!!.exportLogs.setOnClickListener { chooseExportFormat() }
binding!!.clearLogs.setOnClickListener { binding!!.clearLogs.setOnClickListener {
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle("Clear logs?") .setTitle("Clear logs?")
@@ -90,4 +98,32 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
ComponentName(requireContext(), NotificationCaptureService::class.java), ComponentName(requireContext(), NotificationCaptureService::class.java),
) )
} }
private fun chooseExportFormat() {
MaterialAlertDialogBuilder(requireContext())
.setItems(arrayOf("CSV", "Formatted text", "HTML ZIP")) { _, which ->
pendingExport = ExportFormat.entries[which]
createDocument.launch("notification-log.${pendingExport!!.extension}")
}.show()
}
private fun writeExport(uri: Uri) {
val format = pendingExport ?: return
val context = requireContext().applicationContext
Thread {
val entries = EncryptedNotificationLogStore(context).readAll()
val settings = LogViewSettingsStore(context).load()
context.contentResolver.openOutputStream(uri)?.use { output -> when (format) {
ExportFormat.CSV -> output.write(LogExporter.csv(entries, settings).encodeToByteArray())
ExportFormat.FORMATTED -> output.write(LogExporter.formatted(entries, settings).encodeToByteArray())
ExportFormat.HTML_ZIP -> ZipOutputStream(output).use { zip ->
zip.putNextEntry(java.util.zip.ZipEntry("notification-log.html"))
zip.write(LogExporter.html(entries, settings).encodeToByteArray())
zip.closeEntry()
}
} }
}.start()
}
private enum class ExportFormat(val extension: String) { CSV("csv"), FORMATTED("txt"), HTML_ZIP("zip") }
} }
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="@dimen/page_padding"> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="@dimen/page_padding">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"><Button android:id="@+id/notification_access" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Enable notification access"/><Button android:id="@+id/clear_logs" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Clear logs" /></LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"><Button android:id="@+id/notification_access" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Enable notification access"/><Button android:id="@+id/export_logs" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Export logs"/><Button android:id="@+id/clear_logs" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Clear logs" /></LinearLayout>
<ScrollView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <ScrollView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical">
<TextView android:id="@+id/empty_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="No logs yet." /> <TextView android:id="@+id/empty_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="No logs yet." />