List installed apps in event settings
This commit is contained in:
@@ -2,12 +2,16 @@ package se.ajpanton.notificationlog
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.CheckBox
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import se.ajpanton.notificationlog.databinding.FragmentEventSettingsBinding
|
import se.ajpanton.notificationlog.databinding.FragmentEventSettingsBinding
|
||||||
import se.ajpanton.notificationlog.settings.AppRuleMode
|
import se.ajpanton.notificationlog.settings.AppRuleMode
|
||||||
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.capture.SeenApps
|
||||||
|
|
||||||
class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) {
|
class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) {
|
||||||
private var binding: FragmentEventSettingsBinding? = null
|
private var binding: FragmentEventSettingsBinding? = null
|
||||||
@@ -47,6 +51,7 @@ class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) {
|
|||||||
binding!!.onlySeenToggle.setOnCheckedChangeListener { _, checked -> save { copy(onlySeenApps = checked) } }
|
binding!!.onlySeenToggle.setOnCheckedChangeListener { _, checked -> save { copy(onlySeenApps = checked) } }
|
||||||
binding!!.seenFirstToggle.setOnCheckedChangeListener { _, checked -> save { copy(seenAppsFirst = checked) } }
|
binding!!.seenFirstToggle.setOnCheckedChangeListener { _, checked -> save { copy(seenAppsFirst = checked) } }
|
||||||
binding!!.routineUpdatesToggle.setOnCheckedChangeListener { _, checked -> save { copy(ignoreRoutineUpdates = checked) } }
|
binding!!.routineUpdatesToggle.setOnCheckedChangeListener { _, checked -> save { copy(ignoreRoutineUpdates = checked) } }
|
||||||
|
reloadAppList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun save(change: se.ajpanton.notificationlog.settings.LoggingRule.() -> se.ajpanton.notificationlog.settings.LoggingRule) {
|
private fun save(change: se.ajpanton.notificationlog.settings.LoggingRule.() -> se.ajpanton.notificationlog.settings.LoggingRule) {
|
||||||
@@ -60,6 +65,64 @@ class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) {
|
|||||||
binding!!.onlySeenToggle.text = if (rule.onlySeenApps) "Show only seen apps" else "Show all apps"
|
binding!!.onlySeenToggle.text = if (rule.onlySeenApps) "Show only seen apps" else "Show all apps"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun reloadAppList() {
|
||||||
|
val container = binding!!.appList
|
||||||
|
container.removeAllViews()
|
||||||
|
val rule = store.ruleFor(type)
|
||||||
|
val seen = SeenApps.snapshot()
|
||||||
|
val apps = requireContext().packageManager.getInstalledApplications(0)
|
||||||
|
.map { info ->
|
||||||
|
AppRow(
|
||||||
|
label = requireContext().packageManager.getApplicationLabel(info).toString(),
|
||||||
|
packageName = info.packageName,
|
||||||
|
seen = info.packageName in seen,
|
||||||
|
selected = info.packageName in rule.selectedPackages,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label })
|
||||||
|
val shown = if (rule.onlySeenApps) apps.filter { it.seen || it.selected } else apps
|
||||||
|
val seenRows = shown.filter { it.seen }
|
||||||
|
val otherRows = shown.filterNot { it.seen }
|
||||||
|
if (!rule.onlySeenApps && rule.seenAppsFirst && seenRows.isNotEmpty()) {
|
||||||
|
seenRows.forEach { container.addView(appView(it)) }
|
||||||
|
if (otherRows.isNotEmpty()) container.addView(separator())
|
||||||
|
otherRows.forEach { container.addView(appView(it)) }
|
||||||
|
} else if (rule.onlySeenApps && seenRows.isNotEmpty() && otherRows.isNotEmpty()) {
|
||||||
|
seenRows.forEach { container.addView(appView(it)) }
|
||||||
|
container.addView(separator())
|
||||||
|
otherRows.forEach { container.addView(appView(it)) }
|
||||||
|
} else {
|
||||||
|
shown.forEach { container.addView(appView(it)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appView(row: AppRow): View = LinearLayout(requireContext()).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
val checkbox = CheckBox(context).apply { isChecked = row.selected }
|
||||||
|
checkbox.setOnCheckedChangeListener { _, checked ->
|
||||||
|
val current = store.ruleFor(type)
|
||||||
|
val packages = current.selectedPackages.toMutableSet().apply {
|
||||||
|
if (checked) add(row.packageName) else remove(row.packageName)
|
||||||
|
}
|
||||||
|
store.save(type, current.copy(selectedPackages = packages))
|
||||||
|
// Deliberately do not reload: an unchecked unseen row must remain reachable.
|
||||||
|
}
|
||||||
|
addView(checkbox)
|
||||||
|
addView(LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||||
|
addView(TextView(context).apply { text = row.label; textSize = 16f; setTypeface(typeface, 1) })
|
||||||
|
addView(TextView(context).apply { text = row.packageName; textSize = 12f })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun separator() = View(requireContext()).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 1).apply { setMargins(0, 12, 0, 12) }
|
||||||
|
setBackgroundColor(0x33000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class AppRow(val label: String, val packageName: String, val seen: Boolean, val selected: Boolean)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ARG_TITLE = "title"
|
private const val ARG_TITLE = "title"
|
||||||
private const val ARG_TYPE = "type"
|
private const val ARG_TYPE = "type"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class NotificationCaptureService : NotificationListenerService() {
|
|||||||
super.onListenerConnected()
|
super.onListenerConnected()
|
||||||
getActiveNotifications()?.forEach { sbn ->
|
getActiveNotifications()?.forEach { sbn ->
|
||||||
val snapshot = NotificationContents.snapshot(sbn)
|
val snapshot = NotificationContents.snapshot(sbn)
|
||||||
|
SeenApps.markSeen(snapshot.packageName)
|
||||||
activeNotifications[snapshot.key] = snapshot
|
activeNotifications[snapshot.key] = snapshot
|
||||||
record(snapshot, NotificationAction.ALREADY_ACTIVE, LoggingType.APPEARING, includeContents = true)
|
record(snapshot, NotificationAction.ALREADY_ACTIVE, LoggingType.APPEARING, includeContents = true)
|
||||||
}
|
}
|
||||||
@@ -40,6 +41,7 @@ class NotificationCaptureService : NotificationListenerService() {
|
|||||||
|
|
||||||
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||||
val snapshot = NotificationContents.snapshot(sbn)
|
val snapshot = NotificationContents.snapshot(sbn)
|
||||||
|
SeenApps.markSeen(snapshot.packageName)
|
||||||
val previous = activeNotifications.put(snapshot.key, snapshot)
|
val previous = activeNotifications.put(snapshot.key, snapshot)
|
||||||
when {
|
when {
|
||||||
previous == null -> record(snapshot, NotificationAction.APPEARED, LoggingType.APPEARING, includeContents = true)
|
previous == null -> record(snapshot, NotificationAction.APPEARED, LoggingType.APPEARING, includeContents = true)
|
||||||
@@ -54,6 +56,7 @@ class NotificationCaptureService : NotificationListenerService() {
|
|||||||
reason: Int,
|
reason: Int,
|
||||||
) {
|
) {
|
||||||
val snapshot = activeNotifications.remove(sbn.key) ?: NotificationContents.snapshot(sbn)
|
val snapshot = activeNotifications.remove(sbn.key) ?: NotificationContents.snapshot(sbn)
|
||||||
|
SeenApps.markSeen(snapshot.packageName)
|
||||||
record(snapshot, actionForRemoval(reason), LoggingType.DISAPPEARING, includeContents = false)
|
record(snapshot, actionForRemoval(reason), LoggingType.DISAPPEARING, includeContents = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package se.ajpanton.notificationlog.capture
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
/** Process/boot-lifetime record of packages seen by the listener. */
|
||||||
|
object SeenApps {
|
||||||
|
private val packages = ConcurrentHashMap.newKeySet<String>()
|
||||||
|
fun markSeen(packageName: String) { packages += packageName }
|
||||||
|
fun snapshot(): Set<String> = packages.toSet()
|
||||||
|
}
|
||||||
@@ -7,6 +7,6 @@
|
|||||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/only_seen_toggle" android:layout_width="match_parent" android:layout_height="wrap_content" />
|
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/only_seen_toggle" android:layout_width="match_parent" android:layout_height="wrap_content" />
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/seen_first_toggle" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Show seen apps at the top" />
|
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/seen_first_toggle" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Show seen apps at the top" />
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/routine_updates_toggle" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Ignore routine updates" />
|
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/routine_updates_toggle" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Ignore routine updates" />
|
||||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="App list will appear here." android:textAppearance="?attr/textAppearanceBody2" />
|
<LinearLayout android:id="@+id/app_list" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:orientation="vertical" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
Reference in New Issue
Block a user