Use lifecycle-scoped IO for screen work
This commit is contained in:
@@ -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,
|
||||||
@@ -130,16 +135,15 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
|
|||||||
selected = info.packageName in rule.selectedPackages,
|
selected = info.packageName in rule.selectedPackages,
|
||||||
hasEventOverride = perAppEventSettings.hasOverride(info.packageName),
|
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) {
|
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,27 +71,25 @@ 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 {
|
text = "[image]"
|
||||||
text = "[image]"
|
setTextColor(Color.WHITE)
|
||||||
setTextColor(Color.WHITE)
|
textSize = 18f
|
||||||
textSize = 18f
|
}, FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER))
|
||||||
}, FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER))
|
} else {
|
||||||
} else {
|
image.setImageBitmap(bitmap)
|
||||||
image.setImageBitmap(bitmap)
|
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,17 +108,25 @@ 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 logStore = EncryptedNotificationLogStore(context)
|
val result = runCatching {
|
||||||
val settings = LogViewSettingsStore(context).load()
|
withContext(Dispatchers.IO) {
|
||||||
context.contentResolver.openOutputStream(uri)?.use { output ->
|
val logStore = EncryptedNotificationLogStore(context)
|
||||||
|
val settings = LogViewSettingsStore(context).load()
|
||||||
|
context.contentResolver.openOutputStream(uri)?.use { output ->
|
||||||
when (format) {
|
when (format) {
|
||||||
ExportFormat.CSV -> writeCsv(output.bufferedWriter(Charsets.UTF_8), logStore, settings, context)
|
ExportFormat.CSV -> writeCsv(output.bufferedWriter(Charsets.UTF_8), logStore, settings, context)
|
||||||
ExportFormat.FORMATTED -> writeFormatted(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)
|
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(
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user