Use lifecycle-scoped IO for screen work

This commit is contained in:
ajp_anton
2026-07-27 22:55:09 +00:00
parent a86ea52006
commit 6467d1ab6c
5 changed files with 61 additions and 34 deletions
@@ -17,6 +17,7 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.lifecycle.lifecycleScope
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.switchmaterial.SwitchMaterial
@@ -31,6 +32,9 @@ import se.ajpanton.notificationlog.settings.AppFilterSettingsStore
import se.ajpanton.notificationlog.settings.LoggingRuleStore
import se.ajpanton.notificationlog.settings.LoggingType
import se.ajpanton.notificationlog.settings.PerAppEventSettingsStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
private var binding: FragmentEventSettingsBinding? = null
@@ -121,8 +125,9 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
val rule = store.load()
val seen = SeenApps.snapshot()
val context = requireContext().applicationContext
Thread {
val apps = context.packageManager.getInstalledApplications(0).map { info ->
viewLifecycleOwner.lifecycleScope.launch {
val apps = withContext(Dispatchers.IO) {
context.packageManager.getInstalledApplications(0).map { info ->
ListedApp(
label = context.packageManager.getApplicationLabel(info).toString(),
packageName = info.packageName,
@@ -130,16 +135,15 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
selected = info.packageName in rule.selectedPackages,
hasEventOverride = perAppEventSettings.hasOverride(info.packageName),
)
}
val items = AppListOrdering.items(apps, rule.onlySeenApps, rule.seenAppsFirst)
activity?.runOnUiThread {
if (binding === currentBinding && generation == appLoadGeneration) {
appAdapter.submit(items)
currentBinding.appListLoading.visibility = View.GONE
currentBinding.appListRefresh.isRefreshing = false
}
}
}.start()
val items = AppListOrdering.items(apps, rule.onlySeenApps, rule.seenAppsFirst)
if (binding === currentBinding && generation == appLoadGeneration) {
appAdapter.submit(items)
currentBinding.appListLoading.visibility = View.GONE
currentBinding.appListRefresh.isRefreshing = false
}
}
}
private fun setPackageSelected(packageName: String, selected: Boolean) {
@@ -19,7 +19,11 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import se.ajpanton.notificationlog.data.EncryptedImageStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/** Full-screen image viewer kept within MainActivity so opening it does not re-lock the app. */
class ImageViewerDialogFragment : DialogFragment() {
@@ -67,27 +71,25 @@ class ImageViewerDialogFragment : DialogFragment() {
val targetWidth = resources.displayMetrics.widthPixels
val targetHeight = resources.displayMetrics.heightPixels
val appContext = requireContext().applicationContext
Thread {
val bitmap = runCatching {
lifecycleScope.launch {
val bitmap = withContext(Dispatchers.IO) { runCatching {
EncryptedImageStore(appContext).read(imageId)?.let { bytes ->
decodeForViewer(bytes, targetWidth, targetHeight)
}
}.getOrNull()
activity?.runOnUiThread {
if (!isAdded) return@runOnUiThread
progress.visibility = View.GONE
if (bitmap == null) {
root.addView(TextView(requireContext()).apply {
text = "[image]"
setTextColor(Color.WHITE)
textSize = 18f
}, FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER))
} else {
image.setImageBitmap(bitmap)
image.visibility = View.VISIBLE
}
}.getOrNull() }
if (!isAdded) return@launch
progress.visibility = View.GONE
if (bitmap == null) {
root.addView(TextView(requireContext()).apply {
text = "[image]"
setTextColor(Color.WHITE)
textSize = 18f
}, FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER))
} else {
image.setImageBitmap(bitmap)
image.visibility = View.VISIBLE
}
}.start()
}
}
private fun decodeForViewer(bytes: ByteArray, targetWidth: Int, targetHeight: Int): Bitmap? {
@@ -7,6 +7,7 @@ import android.content.Intent
import android.provider.Settings
import android.view.View
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import se.ajpanton.notificationlog.databinding.FragmentSettingsBinding
import se.ajpanton.notificationlog.settings.LogField
@@ -19,6 +20,9 @@ import se.ajpanton.notificationlog.export.LogExporter
import se.ajpanton.notificationlog.settings.StorageLimits
import se.ajpanton.notificationlog.settings.StorageLimitsStore
import java.util.zip.ZipOutputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SettingsFragment : Fragment(R.layout.fragment_settings) {
private var binding: FragmentSettingsBinding? = null
@@ -64,7 +68,9 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
}
store.save(StorageLimits(logLimit * StorageLimitsStore.MEBIBYTE, imageLimit * StorageLimitsStore.MEBIBYTE))
val appContext = requireContext().applicationContext
Thread { EncryptedNotificationLogStore(appContext).enforceLimits() }.start()
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
EncryptedNotificationLogStore(appContext).enforceLimits()
}
}
}
@@ -73,7 +79,11 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
.setTitle("Clear logs?")
.setMessage("This permanently removes every stored notification log and copied image.")
.setNegativeButton("Cancel", null)
.setPositiveButton("Clear") { _, _ -> EncryptedNotificationLogStore(requireContext()).clear() }
.setPositiveButton("Clear") { _, _ ->
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
EncryptedNotificationLogStore(requireContext().applicationContext).clear()
}
}
.show()
}
@@ -98,17 +108,25 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
private fun writeExport(uri: Uri) {
val format = pendingExport ?: return
val context = requireContext().applicationContext
Thread {
val logStore = EncryptedNotificationLogStore(context)
val settings = LogViewSettingsStore(context).load()
context.contentResolver.openOutputStream(uri)?.use { output ->
viewLifecycleOwner.lifecycleScope.launch {
val result = runCatching {
withContext(Dispatchers.IO) {
val logStore = EncryptedNotificationLogStore(context)
val settings = LogViewSettingsStore(context).load()
context.contentResolver.openOutputStream(uri)?.use { output ->
when (format) {
ExportFormat.CSV -> writeCsv(output.bufferedWriter(Charsets.UTF_8), logStore, settings, context)
ExportFormat.FORMATTED -> writeFormatted(output.bufferedWriter(Charsets.UTF_8), logStore, settings, context)
ExportFormat.HTML_ZIP -> writeHtmlZip(ZipOutputStream(output), logStore, settings, context)
}
}
}.start()
}
}
pendingExport = null
result.exceptionOrNull()?.let { error ->
android.widget.Toast.makeText(requireContext(), "Export failed: ${error.message ?: "unknown error"}", android.widget.Toast.LENGTH_LONG).show()
}
}
}
private fun writeCsv(