6 Commits
9 changed files with 248 additions and 36 deletions
@@ -8,6 +8,7 @@ import android.content.res.Configuration
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.WindowManager
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -23,23 +24,27 @@ import androidx.fragment.app.commit
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import se.ajpanton.notificationlog.databinding.ActivityMainBinding import se.ajpanton.notificationlog.databinding.ActivityMainBinding
import se.ajpanton.notificationlog.settings.AppLockStore import se.ajpanton.notificationlog.settings.AppLockStore
import se.ajpanton.notificationlog.settings.LogAppFilterStore
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var drawerToggle: ActionBarDrawerToggle private lateinit var drawerToggle: ActionBarDrawerToggle
private var permanentSidebar = false private var permanentSidebar = false
private var needsUnlock = true private var needsUnlock = true
private lateinit var appLockStore: AppLockStore
private var currentItemId = R.id.nav_view_logs private var currentItemId = R.id.nav_view_logs
private var drawerNavigationBasePaddingLeft: Int? = null private var drawerNavigationBasePaddingLeft: Int? = null
private var permanentNavigationBasePaddingLeft: Int? = null private var permanentNavigationBasePaddingLeft: Int? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
appLockStore = AppLockStore(this)
updateSensitiveWindowPolicy(appLockStore.enabled)
// Keep every app surface below the system bars rather than relying on edge-to-edge drawing. // Keep every app surface below the system bars rather than relying on edge-to-edge drawing.
WindowCompat.setDecorFitsSystemWindows(window, true) WindowCompat.setDecorFitsSystemWindows(window, true)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
showLockedOverlay(AppLockStore(this).enabled) showLockedOverlay(appLockStore.enabled)
val isNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == val isNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES Configuration.UI_MODE_NIGHT_YES
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightNavigationBars = !isNightMode WindowInsetsControllerCompat(window, binding.root).isAppearanceLightNavigationBars = !isNightMode
@@ -85,7 +90,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (needsUnlock && AppLockStore(this).enabled) { if (needsUnlock && appLockStore.enabled) {
showLockedOverlay(true) showLockedOverlay(true)
requestUnlock() requestUnlock()
} else { } else {
@@ -97,7 +102,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
super.onStop() super.onStop()
if (!isChangingConfigurations) { if (!isChangingConfigurations) {
needsUnlock = true needsUnlock = true
if (AppLockStore(this).enabled) showLockedOverlay(true) if (appLockStore.enabled) showLockedOverlay(true)
} }
} }
@@ -119,6 +124,18 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
binding.lockOverlay.visibility = if (visible) View.VISIBLE else View.GONE binding.lockOverlay.visibility = if (visible) View.VISIBLE else View.GONE
} }
/** Called immediately when the Settings switch changes, before a task snapshot can be captured. */
fun setAppLockEnabled(enabled: Boolean) {
appLockStore.enabled = enabled
updateSensitiveWindowPolicy(enabled)
}
private fun updateSensitiveWindowPolicy(locked: Boolean) {
setRecentsScreenshotEnabled(!locked)
if (locked) window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
else window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
override fun onNavigationItemSelected(item: MenuItem): Boolean { override fun onNavigationItemSelected(item: MenuItem): Boolean {
navigateTo(item.itemId) navigateTo(item.itemId)
if (!permanentSidebar) { if (!permanentSidebar) {
@@ -146,6 +163,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
runOnCommit(::applyPageTitleVisibility) runOnCommit(::applyPageTitleVisibility)
} }
title = getString(page.titleRes) title = getString(page.titleRes)
invalidateOptionsMenu()
} }
private fun setupNavigationView(navigationView: NavigationView) { private fun setupNavigationView(navigationView: NavigationView) {
@@ -289,6 +307,38 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
drawerToggle.drawerArrowDrawable.color = getColor(R.color.on_primary) drawerToggle.drawerArrowDrawable.color = getColor(R.color.on_primary)
} }
fun updateLogFilterAction() {
if (currentItemId == R.id.nav_view_logs) invalidateOptionsMenu()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
return updateToolbarActions(menu)
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
menu.clear()
return updateToolbarActions(menu)
}
private fun updateToolbarActions(menu: Menu): Boolean {
if (currentItemId != R.id.nav_view_logs) return false
val active = LogAppFilterStore(this).selectedPackages().isNotEmpty()
menu.add(Menu.NONE, MENU_LOG_FILTER, Menu.NONE, "Filter logs by app").apply {
setIcon(if (active) R.drawable.ic_log_filter_active else R.drawable.ic_log_filter)
icon?.setTint(getColor(R.color.on_primary))
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == MENU_LOG_FILTER) {
(supportFragmentManager.findFragmentById(R.id.content_frame) as? ViewLogsFragment)?.showAppFilter()
return true
}
return super.onOptionsItemSelected(item)
}
private enum class Page( private enum class Page(
val menuId: Int, val menuId: Int,
val titleRes: Int, val titleRes: Int,
@@ -307,6 +357,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
private companion object { private companion object {
const val STATE_CURRENT_ITEM_ID = "current_item_id" const val STATE_CURRENT_ITEM_ID = "current_item_id"
const val MENU_LOG_FILTER = 1
const val PERMANENT_SIDEBAR_FOLDED_WIDTH_MULTIPLIER = 1.1f const val PERMANENT_SIDEBAR_FOLDED_WIDTH_MULTIPLIER = 1.1f
} }
} }
@@ -19,6 +19,7 @@ import se.ajpanton.notificationlog.capture.NotificationCaptureService
import se.ajpanton.notificationlog.export.LogExporter 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 se.ajpanton.notificationlog.settings.LogAppFilterStore
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -38,7 +39,9 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
binding!!.notificationAccess.setOnClickListener { startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)) } binding!!.notificationAccess.setOnClickListener { startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)) }
val lockStore = AppLockStore(requireContext()) val lockStore = AppLockStore(requireContext())
binding!!.appLock.isChecked = lockStore.enabled binding!!.appLock.isChecked = lockStore.enabled
binding!!.appLock.setOnCheckedChangeListener { _, enabled -> lockStore.enabled = enabled } binding!!.appLock.setOnCheckedChangeListener { _, enabled ->
(activity as? MainActivity)?.setAppLockEnabled(enabled) ?: run { lockStore.enabled = enabled }
}
bindStorageLimits() bindStorageLimits()
} }
override fun onDestroyView() { binding = null; super.onDestroyView() } override fun onDestroyView() { binding = null; super.onDestroyView() }
@@ -75,16 +78,29 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
} }
private fun confirmClearLogs() { private fun confirmClearLogs() {
MaterialAlertDialogBuilder(requireContext()) val filterStore = LogAppFilterStore(requireContext())
val dialog = MaterialAlertDialogBuilder(requireContext())
.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") { _, _ -> .setPositiveButton("Clear") { _, _ ->
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { clearLogs()
EncryptedNotificationLogStore(requireContext().applicationContext).clear()
}
} }
.show() if (filterStore.selectedPackages().isNotEmpty()) {
dialog.setNeutralButton("Clear and reset filter") { _, _ ->
filterStore.clear()
(activity as? MainActivity)?.updateLogFilterAction()
clearLogs()
}
}
dialog.show()
}
private fun clearLogs() {
val appContext = requireContext().applicationContext
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
EncryptedNotificationLogStore(appContext).clear()
}
} }
private fun chooseExportFormat() { private fun chooseExportFormat() {
@@ -12,6 +12,7 @@ import android.content.ComponentName
import android.app.NotificationManager import android.app.NotificationManager
import android.text.TextPaint import android.text.TextPaint
import android.text.TextUtils import android.text.TextUtils
import android.view.Gravity
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
@@ -26,6 +27,7 @@ import se.ajpanton.notificationlog.model.NotificationLogEntry
import se.ajpanton.notificationlog.settings.LogField import se.ajpanton.notificationlog.settings.LogField
import se.ajpanton.notificationlog.settings.LogViewSettings import se.ajpanton.notificationlog.settings.LogViewSettings
import se.ajpanton.notificationlog.settings.LogViewSettingsStore import se.ajpanton.notificationlog.settings.LogViewSettingsStore
import se.ajpanton.notificationlog.settings.LogAppFilterStore
import se.ajpanton.notificationlog.settings.TimestampFormatter import se.ajpanton.notificationlog.settings.TimestampFormatter
import se.ajpanton.notificationlog.settings.DisplayEvent import se.ajpanton.notificationlog.settings.DisplayEvent
import se.ajpanton.notificationlog.capture.NotificationCaptureService import se.ajpanton.notificationlog.capture.NotificationCaptureService
@@ -49,10 +51,12 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
private var noMoreRows = false private var noMoreRows = false
private var loadGeneration = 0 private var loadGeneration = 0
private val logAdapter = LogAdapter() private val logAdapter = LogAdapter()
private lateinit var appFilterStore: LogAppFilterStore
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentViewLogsBinding.bind(view) binding = FragmentViewLogsBinding.bind(view)
appFilterStore = LogAppFilterStore(requireContext())
binding!!.logsRefresh.setOnRefreshListener { loadLogs(reset = true) } binding!!.logsRefresh.setOnRefreshListener { loadLogs(reset = true) }
binding!!.logList.layoutManager = LinearLayoutManager(requireContext()) binding!!.logList.layoutManager = LinearLayoutManager(requireContext())
binding!!.logList.adapter = logAdapter binding!!.logList.adapter = logAdapter
@@ -82,7 +86,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
} }
private fun loadLogs(reset: Boolean) { private fun loadLogs(reset: Boolean) {
if (loading) return if (loading && !reset) return
if (!reset && noMoreRows) return if (!reset && noMoreRows) return
val context = requireContext().applicationContext val context = requireContext().applicationContext
if (reset) { if (reset) {
@@ -98,30 +102,93 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
val page = withContext(Dispatchers.IO) { val page = withContext(Dispatchers.IO) {
EncryptedNotificationLogStore(context).readNewest(nextCursor, PAGE_SIZE) EncryptedNotificationLogStore(context).readNewest(nextCursor, PAGE_SIZE)
} }
if (generation == loadGeneration) { if (generation != loadGeneration) return@launch
nextCursor = page.nextCursor nextCursor = page.nextCursor
noMoreRows = nextCursor == null noMoreRows = nextCursor == null
val addedRows = page.entries.map(::LogRow) val visibleBefore = visibleRows(LogViewSettingsStore(requireContext()).load()).size
rows += addedRows rows += page.entries.map(::LogRow)
binding?.let(::render) binding?.let(::render)
} val visibleAfter = visibleRows(LogViewSettingsStore(requireContext()).load()).size
loading = false val skipFilteredPage = appFilterStore.selectedPackages().isNotEmpty() &&
binding?.logsRefresh?.isRefreshing = false visibleAfter == visibleBefore && !noMoreRows
loading = false
binding?.logsRefresh?.isRefreshing = false
if (skipFilteredPage) loadLogs(reset = false)
} }
} }
private fun render(view: FragmentViewLogsBinding) { private fun render(view: FragmentViewLogsBinding) {
view.emptyView.visibility = if (rows.isEmpty()) View.VISIBLE else View.GONE val settings = LogViewSettingsStore(requireContext()).load()
val visibleRows = visibleRows(settings)
view.emptyView.visibility = if (visibleRows.isEmpty()) View.VISIBLE else View.GONE
view.emptyView.text = if (hasNotificationAccess()) { view.emptyView.text = if (hasNotificationAccess()) {
"No logs yet." if (appFilterStore.selectedPackages().isEmpty()) "No logs yet." else "No logs match the app filter."
} else { } else {
"Notification access is disabled. Enable it in Settings." "Notification access is disabled. Enable it in Settings."
} }
val settings = LogViewSettingsStore(requireContext()).load()
val visibleRows = rows.filter { eventFor(it.entry.action) in settings.visibleEvents }
logAdapter.submit(visibleRows, settings, metadataColumnWidth(view.logList.width, visibleRows, settings)) logAdapter.submit(visibleRows, settings, metadataColumnWidth(view.logList.width, visibleRows, settings))
} }
/** Called by MainActivity's header action. Selections persist until Reset is chosen. */
fun showAppFilter() {
val appContext = requireContext().applicationContext
viewLifecycleOwner.lifecycleScope.launch {
val apps = withContext(Dispatchers.IO) {
val byPackage = linkedMapOf<String, LoggedApp>()
appFilterStore.selectedApps().forEach { app ->
val name = app.name.takeUnless { it == app.packageName } ?: runCatching {
appContext.packageManager.getApplicationLabel(
appContext.packageManager.getApplicationInfo(app.packageName, 0),
).toString()
}.getOrDefault(app.packageName)
byPackage[app.packageName] = LoggedApp(name, app.packageName)
}
EncryptedNotificationLogStore(appContext).forEachNewest { entry ->
byPackage[entry.packageName] = LoggedApp(entry.appName, entry.packageName)
}
byPackage.values.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
}
if (!isAdded) return@launch
val selected = appFilterStore.selectedPackages().toMutableSet()
var changed = false
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle("Filter logs by app")
.setMultiChoiceItems(
apps.map { "${it.name}\n${it.packageName}" }.toTypedArray(),
apps.map { it.packageName in selected }.toBooleanArray(),
) { _, which, checked ->
if (checked) selected += apps[which].packageName else selected -= apps[which].packageName
appFilterStore.save(apps.filter { it.packageName in selected }.map { app ->
LogAppFilterStore.App(app.name, app.packageName)
})
changed = true
}
.setNeutralButton("Reset") { _, _ ->
appFilterStore.clear()
changed = true
}
.setPositiveButton("Close", null)
.create()
dialog.setOnDismissListener {
if (changed) refreshAppFilter()
}
dialog.show()
}
}
private fun refreshAppFilter() {
(activity as? MainActivity)?.updateLogFilterAction()
loadLogs(reset = true)
}
private fun visibleRows(settings: LogViewSettings): List<LogRow> {
val selectedPackages = appFilterStore.selectedPackages()
return rows.filter { row ->
eventFor(row.entry.action) in settings.visibleEvents &&
(selectedPackages.isEmpty() || row.entry.packageName in selectedPackages)
}
}
private fun logRowView(row: LogRow, settings: LogViewSettings, metadataColumnWidth: Int): View = LinearLayout(requireContext()).apply { private fun logRowView(row: LogRow, settings: LogViewSettings, metadataColumnWidth: Int): View = LinearLayout(requireContext()).apply {
orientation = LinearLayout.HORIZONTAL orientation = LinearLayout.HORIZONTAL
gravity = android.view.Gravity.TOP gravity = android.view.Gravity.TOP
@@ -224,10 +291,10 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
} }
addView(message) addView(message)
if (expanded && row.entry.action == NotificationAction.EDITED && !row.entry.previousContents.isNullOrEmpty()) { if (expanded && row.entry.action == NotificationAction.EDITED && !row.entry.previousContents.isNullOrEmpty()) {
addView(previousDivider())
addView(TextView(context).apply { addView(TextView(context).apply {
text = "Previous: ${row.entry.previousContents}" text = row.entry.previousContents
setLineSpacing(0f, 1f) setLineSpacing(0f, 1f)
setPadding(0, dp(4), 0, 0)
}) })
} }
if (expanded && row.entry.imageId != null) { if (expanded && row.entry.imageId != null) {
@@ -260,6 +327,24 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
setOnLongClickListener { showDeleteDialog(row.entry.id) } setOnLongClickListener { showDeleteDialog(row.entry.id) }
} }
private fun previousDivider(): LinearLayout = LinearLayout(requireContext()).apply {
gravity = Gravity.CENTER_VERTICAL
setPadding(0, dp(8), 0, dp(4))
fun line() = View(context).apply {
setBackgroundColor(ContextCompat.getColor(context, R.color.log_row_separator))
layoutParams = LinearLayout.LayoutParams(0, dp(1), 1f)
}
addView(line())
addView(TextView(context).apply {
text = "Previous"
textSize = 12f
setTextColor(ContextCompat.getColor(context, R.color.on_surface))
setBackgroundColor(ContextCompat.getColor(context, R.color.window_background))
setPadding(dp(8), 0, dp(8), 0)
})
addView(line())
}
private fun metadataValues(entry: NotificationLogEntry, settings: LogViewSettings): List<MetadataValue> = buildList { private fun metadataValues(entry: NotificationLogEntry, settings: LogViewSettings): List<MetadataValue> = buildList {
if (LogField.TIMESTAMP in settings.visibleFields) add(MetadataValue(timestamp(entry.recordedAtEpochMillis, settings, entry.eventTimeZoneId), 12f, isTimestamp = true)) if (LogField.TIMESTAMP in settings.visibleFields) add(MetadataValue(timestamp(entry.recordedAtEpochMillis, settings, entry.eventTimeZoneId), 12f, isTimestamp = true))
if (LogField.APP_NAME in settings.visibleFields) add(MetadataValue(entry.appName, 15f, indented = true, bold = true)) if (LogField.APP_NAME in settings.visibleFields) add(MetadataValue(entry.appName, 15f, indented = true, bold = true))
@@ -463,6 +548,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
) )
private data class LogRow(val entry: NotificationLogEntry) private data class LogRow(val entry: NotificationLogEntry)
private data class LoggedApp(val name: String, val packageName: String)
private data class MetadataValue( private data class MetadataValue(
val text: String, val text: String,
val textSize: Float, val textSize: Float,
@@ -56,10 +56,12 @@ object NotificationContents {
/** MessagingStyle also populates generic title/text fields with an alternate rendering. */ /** MessagingStyle also populates generic title/text fields with an alternate rendering. */
private fun messagingContents(extras: Bundle): String? { private fun messagingContents(extras: Bundle): String? {
val parts = linkedSetOf<String>() val parts = linkedSetOf<String>()
Notification.MessagingStyle.Message.getMessagesFromBundleArray( listOf(Notification.EXTRA_HISTORIC_MESSAGES, Notification.EXTRA_MESSAGES).forEach { key ->
extras.getParcelableArray(Notification.EXTRA_MESSAGES, Bundle::class.java), Notification.MessagingStyle.Message.getMessagesFromBundleArray(
).forEach { message -> extras.getParcelableArray(key, Bundle::class.java),
listOfNotNull(message.senderPerson?.name, message.text).joinToString(": ").addTo(parts) ).forEach { message ->
listOfNotNull(message.senderPerson?.name, message.text).joinToString(": ").addTo(parts)
}
} }
return parts.takeIf { it.isNotEmpty() }?.joinToString("\n") return parts.takeIf { it.isNotEmpty() }?.joinToString("\n")
} }
@@ -76,7 +78,11 @@ object NotificationContents {
} == true, } == true,
isGroupSummary = notification.flags and Notification.FLAG_GROUP_SUMMARY != 0, isGroupSummary = notification.flags and Notification.FLAG_GROUP_SUMMARY != 0,
isRoutine = extras?.getBoolean(Notification.EXTRA_SHOW_CHRONOMETER, false) == true || isRoutine = extras?.getBoolean(Notification.EXTRA_SHOW_CHRONOMETER, false) == true ||
extras?.containsKey(Notification.EXTRA_PROGRESS) == true, extras?.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE, false) == true ||
extras?.let {
it.getInt(Notification.EXTRA_PROGRESS_MAX, 0) > 0 &&
it.getInt(Notification.EXTRA_PROGRESS, -1) >= 0
} == true,
) )
} }
@@ -0,0 +1,28 @@
package se.ajpanton.notificationlog.settings
import android.content.Context
import androidx.core.content.edit
class LogAppFilterStore(context: Context) {
private val prefs = context.getSharedPreferences("log-app-filter", Context.MODE_PRIVATE)
fun selectedPackages(): Set<String> = prefs.getStringSet("packages", emptySet()).orEmpty().toSet()
fun selectedApps(): List<App> = selectedPackages().map { packageName ->
App(prefs.getString("name:$packageName", packageName).orEmpty(), packageName)
}
fun save(apps: Collection<App>) = prefs.edit {
val packages = apps.mapTo(mutableSetOf()) { it.packageName }
prefs.all.keys.filter { it.startsWith("name:") && it.removePrefix("name:") !in packages }.forEach(::remove)
putStringSet("packages", packages)
apps.forEach { putString("name:${it.packageName}", it.name) }
}
fun clear() = prefs.edit {
remove("packages")
prefs.all.keys.filter { it.startsWith("name:") }.forEach(::remove)
}
data class App(val name: String, val packageName: String)
}
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M3,5h18l-7,8v5l-4,2v-7z" />
</vector>
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M3,5h18l-7,8v5l-4,2v-7z" />
<path
android:fillColor="#FF000000"
android:pathData="M18,16m-3,0a3,3 0,1 0,6 0a3,3 0,1 0,-6 0" />
</vector>
@@ -46,7 +46,8 @@ class MainActivity : AppCompatActivity() {
when (action) { when (action) {
"post_text" -> postText("Text message") "post_text" -> postText("Text message")
"edit_text" -> postText("Edited message") "edit_text" -> postText("Edited message")
"messaging" -> postMessaging() "messaging" -> postMessaging(updated = false)
"messaging_update" -> postMessaging(updated = true)
"image" -> postImage() "image" -> postImage()
"dismissible" -> postDismissible() "dismissible" -> postDismissible()
"big_text" -> postBigText() "big_text" -> postBigText()
@@ -66,12 +67,14 @@ class MainActivity : AppCompatActivity() {
private fun postText(text: String) = manager.notify(TEXT_ID, base().setContentTitle("Helper").setContentText(text).build()) private fun postText(text: String) = manager.notify(TEXT_ID, base().setContentTitle("Helper").setContentText(text).build())
private fun postMessaging() { private fun postMessaging(updated: Boolean) {
val me = Person.Builder().setName("Me").build() val me = Person.Builder().setName("Me").build()
val alice = Person.Builder().setName("Alice").build() val alice = Person.Builder().setName("Alice").build()
val style = Notification.MessagingStyle(me) val style = Notification.MessagingStyle(me).addMessage("Hello from Alice", 1, alice)
.addMessage("Hello from Alice", 1, alice) if (updated) {
.addMessage("A reply from me", 2, null as Person?) style.addMessage("Another message from Alice", 2, alice)
.addMessage("A reply from me", 3, null as Person?)
}
manager.notify(MESSAGING_ID, base().setSmallIcon(android.R.drawable.ic_dialog_email).setStyle(style).build()) manager.notify(MESSAGING_ID, base().setSmallIcon(android.R.drawable.ic_dialog_email).setStyle(style).build())
} }
@@ -126,6 +129,7 @@ class MainActivity : AppCompatActivity() {
const val GROUP_KEY = "helper-group" const val GROUP_KEY = "helper-group"
val ACTIONS = listOf( val ACTIONS = listOf(
"Post text" to "post_text", "Edit text" to "edit_text", "Post messaging" to "messaging", "Post text" to "post_text", "Edit text" to "edit_text", "Post messaging" to "messaging",
"Update messaging" to "messaging_update",
"Post image" to "image", "Post dismissible" to "dismissible", "Post big text" to "big_text", "Post image" to "image", "Post dismissible" to "dismissible", "Post big text" to "big_text",
"Post very long text" to "very_long_text", "Post very long text" to "very_long_text",
"Post inbox" to "inbox", "Post progress" to "progress", "Update progress" to "progress_update", "Post inbox" to "inbox", "Post progress" to "progress", "Update progress" to "progress_update",
+1 -1
View File
@@ -1 +1 @@
version=1.0 version=1.1