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
+1
View File
@@ -90,6 +90,7 @@ dependencies {
implementation(libs.drawerlayout) implementation(libs.drawerlayout)
implementation(libs.recyclerview) implementation(libs.recyclerview)
implementation(libs.swiperefreshlayout) implementation(libs.swiperefreshlayout)
implementation(libs.lifecycle.runtime.ktx)
testImplementation(libs.junit) testImplementation(libs.junit)
} }
@@ -17,6 +17,7 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.lifecycle.lifecycleScope
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.switchmaterial.SwitchMaterial 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.LoggingRuleStore
import se.ajpanton.notificationlog.settings.LoggingType import se.ajpanton.notificationlog.settings.LoggingType
import se.ajpanton.notificationlog.settings.PerAppEventSettingsStore 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) { class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
private var binding: FragmentEventSettingsBinding? = null private var binding: FragmentEventSettingsBinding? = null
@@ -121,8 +125,9 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
val rule = store.load() val rule = store.load()
val seen = SeenApps.snapshot() val seen = SeenApps.snapshot()
val context = requireContext().applicationContext val context = requireContext().applicationContext
Thread { viewLifecycleOwner.lifecycleScope.launch {
val apps = context.packageManager.getInstalledApplications(0).map { info -> val apps = withContext(Dispatchers.IO) {
context.packageManager.getInstalledApplications(0).map { info ->
ListedApp( ListedApp(
label = context.packageManager.getApplicationLabel(info).toString(), label = context.packageManager.getApplicationLabel(info).toString(),
packageName = info.packageName, packageName = info.packageName,
@@ -131,15 +136,14 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
hasEventOverride = perAppEventSettings.hasOverride(info.packageName), hasEventOverride = perAppEventSettings.hasOverride(info.packageName),
) )
} }
}
val items = AppListOrdering.items(apps, rule.onlySeenApps, rule.seenAppsFirst) val items = AppListOrdering.items(apps, rule.onlySeenApps, rule.seenAppsFirst)
activity?.runOnUiThread {
if (binding === currentBinding && generation == appLoadGeneration) { if (binding === currentBinding && generation == appLoadGeneration) {
appAdapter.submit(items) appAdapter.submit(items)
currentBinding.appListLoading.visibility = View.GONE currentBinding.appListLoading.visibility = View.GONE
currentBinding.appListRefresh.isRefreshing = false currentBinding.appListRefresh.isRefreshing = false
} }
} }
}.start()
} }
private fun setPackageSelected(packageName: String, selected: Boolean) { 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.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import se.ajpanton.notificationlog.data.EncryptedImageStore 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. */ /** Full-screen image viewer kept within MainActivity so opening it does not re-lock the app. */
class ImageViewerDialogFragment : DialogFragment() { class ImageViewerDialogFragment : DialogFragment() {
@@ -67,14 +71,13 @@ class ImageViewerDialogFragment : DialogFragment() {
val targetWidth = resources.displayMetrics.widthPixels val targetWidth = resources.displayMetrics.widthPixels
val targetHeight = resources.displayMetrics.heightPixels val targetHeight = resources.displayMetrics.heightPixels
val appContext = requireContext().applicationContext val appContext = requireContext().applicationContext
Thread { lifecycleScope.launch {
val bitmap = runCatching { val bitmap = withContext(Dispatchers.IO) { runCatching {
EncryptedImageStore(appContext).read(imageId)?.let { bytes -> EncryptedImageStore(appContext).read(imageId)?.let { bytes ->
decodeForViewer(bytes, targetWidth, targetHeight) decodeForViewer(bytes, targetWidth, targetHeight)
} }
}.getOrNull() }.getOrNull() }
activity?.runOnUiThread { if (!isAdded) return@launch
if (!isAdded) return@runOnUiThread
progress.visibility = View.GONE progress.visibility = View.GONE
if (bitmap == null) { if (bitmap == null) {
root.addView(TextView(requireContext()).apply { root.addView(TextView(requireContext()).apply {
@@ -87,7 +90,6 @@ class ImageViewerDialogFragment : DialogFragment() {
image.visibility = View.VISIBLE image.visibility = View.VISIBLE
} }
} }
}.start()
} }
private fun decodeForViewer(bytes: ByteArray, targetWidth: Int, targetHeight: Int): Bitmap? { private fun decodeForViewer(bytes: ByteArray, targetWidth: Int, targetHeight: Int): Bitmap? {
@@ -7,6 +7,7 @@ import android.content.Intent
import android.provider.Settings import android.provider.Settings
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import se.ajpanton.notificationlog.databinding.FragmentSettingsBinding import se.ajpanton.notificationlog.databinding.FragmentSettingsBinding
import se.ajpanton.notificationlog.settings.LogField 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.StorageLimits
import se.ajpanton.notificationlog.settings.StorageLimitsStore import se.ajpanton.notificationlog.settings.StorageLimitsStore
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SettingsFragment : Fragment(R.layout.fragment_settings) { class SettingsFragment : Fragment(R.layout.fragment_settings) {
private var binding: FragmentSettingsBinding? = null 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)) store.save(StorageLimits(logLimit * StorageLimitsStore.MEBIBYTE, imageLimit * StorageLimitsStore.MEBIBYTE))
val appContext = requireContext().applicationContext 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?") .setTitle("Clear logs?")
.setMessage("This permanently removes every stored notification log and copied image.") .setMessage("This permanently removes every stored notification log and copied image.")
.setNegativeButton("Cancel", null) .setNegativeButton("Cancel", null)
.setPositiveButton("Clear") { _, _ -> EncryptedNotificationLogStore(requireContext()).clear() } .setPositiveButton("Clear") { _, _ ->
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
EncryptedNotificationLogStore(requireContext().applicationContext).clear()
}
}
.show() .show()
} }
@@ -98,7 +108,9 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
private fun writeExport(uri: Uri) { private fun writeExport(uri: Uri) {
val format = pendingExport ?: return val format = pendingExport ?: return
val context = requireContext().applicationContext val context = requireContext().applicationContext
Thread { viewLifecycleOwner.lifecycleScope.launch {
val result = runCatching {
withContext(Dispatchers.IO) {
val logStore = EncryptedNotificationLogStore(context) val logStore = EncryptedNotificationLogStore(context)
val settings = LogViewSettingsStore(context).load() val settings = LogViewSettingsStore(context).load()
context.contentResolver.openOutputStream(uri)?.use { output -> context.contentResolver.openOutputStream(uri)?.use { output ->
@@ -108,7 +120,13 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
ExportFormat.HTML_ZIP -> writeHtmlZip(ZipOutputStream(output), 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( private fun writeCsv(
+2
View File
@@ -8,6 +8,7 @@ material = "1.10.0"
drawerLayout = "1.2.0" drawerLayout = "1.2.0"
recyclerView = "1.3.2" recyclerView = "1.3.2"
swipeRefreshLayout = "1.1.0" swipeRefreshLayout = "1.1.0"
lifecycle = "2.8.7"
junit = "4.13.2" junit = "4.13.2"
[libraries] [libraries]
@@ -18,6 +19,7 @@ material = { group = "com.google.android.material", name = "material", version.r
drawerlayout = { group = "androidx.drawerlayout", name = "drawerlayout", version.ref = "drawerLayout" } drawerlayout = { group = "androidx.drawerlayout", name = "drawerlayout", version.ref = "drawerLayout" }
recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerView" } recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerView" }
swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swipeRefreshLayout" } swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swipeRefreshLayout" }
lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
[plugins] [plugins]