Use lifecycle-scoped IO for screen work
This commit is contained in:
@@ -90,6 +90,7 @@ dependencies {
|
||||
implementation(libs.drawerlayout)
|
||||
implementation(libs.recyclerview)
|
||||
implementation(libs.swiperefreshlayout)
|
||||
implementation(libs.lifecycle.runtime.ktx)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user