11 Commits
18 changed files with 286 additions and 52 deletions
+3 -1
View File
@@ -19,6 +19,8 @@ decisions, physical device testing, and final behaviour are still manually revie
You can choose which event types to record, set app-specific allow/block lists,
and suppress routine progress or timer updates. The log view can be tailored,
expanded for long entries, and exported as CSV, aligned text, or an HTML ZIP.
Only the HTML ZIP includes saved notification images; CSV and aligned text use
an `[image]` marker instead.
## Privacy
@@ -42,4 +44,4 @@ Tested Android versions:
- OneUI 8.0 (Android 16) on a Galaxy Z Fold 5
- AOSP Android 14, 15, and 16 (API 34, 35, and 36) on an emulator
- Google APIs Android 15 and 16 (API 35 and 36) on an emulator
- LineageOS 22 on Android 15 and LineageOS 23 on Android 16 on an emulator
- LineageOS 22 on Android 15 and LineageOS 23 on Android 16 on an emulator
@@ -18,7 +18,6 @@ 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
import se.ajpanton.notificationlog.capture.SeenApps
@@ -293,13 +292,13 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
}
}
holder.filter.contentDescription = "Edit logging events for ${app.label}"
holder.filter.imageTintList = ColorStateList.valueOf(
MaterialColors.getColor(
holder.filter,
if (app.hasEventOverride) com.google.android.material.R.attr.colorPrimary
else com.google.android.material.R.attr.colorOnSurface,
),
)
val colors = if (app.hasEventOverride) {
R.color.app_filter_override_background to R.color.app_filter_override_icon
} else {
R.color.app_filter_inherited_background to R.color.app_filter_inherited_icon
}
holder.filter.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(holder.filter.context, colors.first))
holder.filter.imageTintList = ColorStateList.valueOf(ContextCompat.getColor(holder.filter.context, colors.second))
holder.filter.setOnClickListener { onEventFilterClicked(app) }
}
if (holder is SectionTitleHolder && item is AppListItem.SectionTitle) {
@@ -107,7 +107,7 @@ class LogDisplayFragment : Fragment(R.layout.fragment_log_display) {
): LinearLayout = LinearLayout(requireContext()).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(TIMESTAMP_CONTROLS_INDENT_DP), 0, 0, 0)
addDropdown("Timestamp timezone", listOf("Current local timezone", "UTC", "Local timezone when event happened"), TimestampZone.entries.indexOf(currentSettings().timestampZone)) { position ->
addDropdown("Timestamp timezone", listOf("Local timezone when event happened", "Current local timezone", "UTC"), TimestampZone.entries.indexOf(currentSettings().timestampZone)) { position ->
val updated = currentSettings().copy(timestampZone = TimestampZone.entries[position])
saveSettings(updated)
}
@@ -123,7 +123,8 @@ class LogDisplayFragment : Fragment(R.layout.fragment_log_display) {
private fun LinearLayout.addDropdown(label: String, values: List<String>, selected: Int, onSelected: (Int) -> Unit) {
addView(TextView(context).apply { text = label })
addView(Spinner(context).apply {
addView(Spinner(context, Spinner.MODE_DROPDOWN).apply {
setPopupBackgroundDrawable(context.getDrawable(R.drawable.dropdown_popup_background))
adapter = ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, values)
setSelection(selected)
onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener {
@@ -8,6 +8,7 @@ import android.content.res.Configuration
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
@@ -23,23 +24,27 @@ import androidx.fragment.app.commit
import com.google.android.material.navigation.NavigationView
import se.ajpanton.notificationlog.databinding.ActivityMainBinding
import se.ajpanton.notificationlog.settings.AppLockStore
import se.ajpanton.notificationlog.settings.LogAppFilterStore
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
private lateinit var binding: ActivityMainBinding
private lateinit var drawerToggle: ActionBarDrawerToggle
private var permanentSidebar = false
private var needsUnlock = true
private lateinit var appLockStore: AppLockStore
private var currentItemId = R.id.nav_view_logs
private var drawerNavigationBasePaddingLeft: Int? = null
private var permanentNavigationBasePaddingLeft: Int? = null
override fun onCreate(savedInstanceState: Bundle?) {
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.
WindowCompat.setDecorFitsSystemWindows(window, true)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
showLockedOverlay(AppLockStore(this).enabled)
showLockedOverlay(appLockStore.enabled)
val isNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightNavigationBars = !isNightMode
@@ -85,7 +90,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
override fun onResume() {
super.onResume()
if (needsUnlock && AppLockStore(this).enabled) {
if (needsUnlock && appLockStore.enabled) {
showLockedOverlay(true)
requestUnlock()
} else {
@@ -97,7 +102,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
super.onStop()
if (!isChangingConfigurations) {
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
}
/** 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 {
navigateTo(item.itemId)
if (!permanentSidebar) {
@@ -146,6 +163,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
runOnCommit(::applyPageTitleVisibility)
}
title = getString(page.titleRes)
invalidateOptionsMenu()
}
private fun setupNavigationView(navigationView: NavigationView) {
@@ -289,6 +307,38 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
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(
val menuId: Int,
val titleRes: Int,
@@ -307,6 +357,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
private companion object {
const val STATE_CURRENT_ITEM_ID = "current_item_id"
const val MENU_LOG_FILTER = 1
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.settings.StorageLimits
import se.ajpanton.notificationlog.settings.StorageLimitsStore
import se.ajpanton.notificationlog.settings.LogAppFilterStore
import java.util.zip.ZipOutputStream
import kotlinx.coroutines.Dispatchers
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)) }
val lockStore = AppLockStore(requireContext())
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()
}
override fun onDestroyView() { binding = null; super.onDestroyView() }
@@ -75,21 +78,34 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
}
private fun confirmClearLogs() {
MaterialAlertDialogBuilder(requireContext())
val filterStore = LogAppFilterStore(requireContext())
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle("Clear logs?")
.setMessage("This permanently removes every stored notification log and copied image.")
.setNegativeButton("Cancel", null)
.setPositiveButton("Clear") { _, _ ->
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
EncryptedNotificationLogStore(requireContext().applicationContext).clear()
}
clearLogs()
}
.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() {
MaterialAlertDialogBuilder(requireContext())
.setItems(arrayOf("CSV", "Formatted text", "HTML ZIP")) { _, which ->
.setItems(arrayOf("CSV (no images)", "Formatted text (no images)", "HTML ZIP")) { _, which ->
pendingExport = ExportFormat.entries[which]
createDocument.launch("notification-log.${pendingExport!!.extension}")
}
@@ -12,6 +12,7 @@ import android.content.ComponentName
import android.app.NotificationManager
import android.text.TextPaint
import android.text.TextUtils
import android.view.Gravity
import android.view.View
import android.widget.ImageView
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.LogViewSettings
import se.ajpanton.notificationlog.settings.LogViewSettingsStore
import se.ajpanton.notificationlog.settings.LogAppFilterStore
import se.ajpanton.notificationlog.settings.TimestampFormatter
import se.ajpanton.notificationlog.settings.DisplayEvent
import se.ajpanton.notificationlog.capture.NotificationCaptureService
@@ -49,10 +51,12 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
private var noMoreRows = false
private var loadGeneration = 0
private val logAdapter = LogAdapter()
private lateinit var appFilterStore: LogAppFilterStore
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentViewLogsBinding.bind(view)
appFilterStore = LogAppFilterStore(requireContext())
binding!!.logsRefresh.setOnRefreshListener { loadLogs(reset = true) }
binding!!.logList.layoutManager = LinearLayoutManager(requireContext())
binding!!.logList.adapter = logAdapter
@@ -82,7 +86,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
}
private fun loadLogs(reset: Boolean) {
if (loading) return
if (loading && !reset) return
if (!reset && noMoreRows) return
val context = requireContext().applicationContext
if (reset) {
@@ -98,30 +102,93 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
val page = withContext(Dispatchers.IO) {
EncryptedNotificationLogStore(context).readNewest(nextCursor, PAGE_SIZE)
}
if (generation == loadGeneration) {
nextCursor = page.nextCursor
noMoreRows = nextCursor == null
val addedRows = page.entries.map(::LogRow)
rows += addedRows
binding?.let(::render)
}
loading = false
binding?.logsRefresh?.isRefreshing = false
if (generation != loadGeneration) return@launch
nextCursor = page.nextCursor
noMoreRows = nextCursor == null
val visibleBefore = visibleRows(LogViewSettingsStore(requireContext()).load()).size
rows += page.entries.map(::LogRow)
binding?.let(::render)
val visibleAfter = visibleRows(LogViewSettingsStore(requireContext()).load()).size
val skipFilteredPage = appFilterStore.selectedPackages().isNotEmpty() &&
visibleAfter == visibleBefore && !noMoreRows
loading = false
binding?.logsRefresh?.isRefreshing = false
if (skipFilteredPage) loadLogs(reset = false)
}
}
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()) {
"No logs yet."
if (appFilterStore.selectedPackages().isEmpty()) "No logs yet." else "No logs match the app filter."
} else {
"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))
}
/** 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 {
orientation = LinearLayout.HORIZONTAL
gravity = android.view.Gravity.TOP
@@ -224,10 +291,10 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
}
addView(message)
if (expanded && row.entry.action == NotificationAction.EDITED && !row.entry.previousContents.isNullOrEmpty()) {
addView(previousDivider())
addView(TextView(context).apply {
text = "Previous: ${row.entry.previousContents}"
text = row.entry.previousContents
setLineSpacing(0f, 1f)
setPadding(0, dp(4), 0, 0)
})
}
if (expanded && row.entry.imageId != null) {
@@ -260,6 +327,24 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
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 {
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))
@@ -463,6 +548,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
)
private data class LogRow(val entry: NotificationLogEntry)
private data class LoggedApp(val name: String, val packageName: String)
private data class MetadataValue(
val text: String,
val textSize: Float,
@@ -56,10 +56,12 @@ object NotificationContents {
/** MessagingStyle also populates generic title/text fields with an alternate rendering. */
private fun messagingContents(extras: Bundle): String? {
val parts = linkedSetOf<String>()
Notification.MessagingStyle.Message.getMessagesFromBundleArray(
extras.getParcelableArray(Notification.EXTRA_MESSAGES, Bundle::class.java),
).forEach { message ->
listOfNotNull(message.senderPerson?.name, message.text).joinToString(": ").addTo(parts)
listOf(Notification.EXTRA_HISTORIC_MESSAGES, Notification.EXTRA_MESSAGES).forEach { key ->
Notification.MessagingStyle.Message.getMessagesFromBundleArray(
extras.getParcelableArray(key, Bundle::class.java),
).forEach { message ->
listOfNotNull(message.senderPerson?.name, message.text).joinToString(": ").addTo(parts)
}
}
return parts.takeIf { it.isNotEmpty() }?.joinToString("\n")
}
@@ -76,7 +78,11 @@ object NotificationContents {
} == true,
isGroupSummary = notification.flags and Notification.FLAG_GROUP_SUMMARY != 0,
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)
}
@@ -1,7 +1,7 @@
package se.ajpanton.notificationlog.settings
enum class LogField { TIMESTAMP, APP_NAME, PACKAGE_NAME, ACTION, CONTENTS }
enum class TimestampZone { LOCAL_NOW, UTC, EVENT_LOCAL }
enum class TimestampZone { EVENT_LOCAL, LOCAL_NOW, UTC }
enum class TimestampDateFormat { SYSTEM_DEFAULT, YEAR_MONTH_DAY, DAY_MONTH_YEAR, MONTH_DAY_YEAR }
enum class TimestampClockFormat { SYSTEM_DEFAULT, HOUR_24, HOUR_12 }
enum class DisplayEvent { APPEARING, DISAPPEARING, EDITS }
@@ -9,7 +9,7 @@ enum class DisplayEvent { APPEARING, DISAPPEARING, EDITS }
data class LogViewSettings(
val visibleFields: Set<LogField> = setOf(LogField.TIMESTAMP, LogField.APP_NAME, LogField.ACTION, LogField.CONTENTS),
val order: List<LogField> = LogField.entries,
val timestampZone: TimestampZone = TimestampZone.LOCAL_NOW,
val timestampZone: TimestampZone = TimestampZone.EVENT_LOCAL,
val timestampDateFormat: TimestampDateFormat = TimestampDateFormat.SYSTEM_DEFAULT,
val timestampClockFormat: TimestampClockFormat = TimestampClockFormat.SYSTEM_DEFAULT,
val visibleEvents: Set<DisplayEvent> = DisplayEvent.entries.toSet(),
@@ -9,7 +9,7 @@ class LogViewSettingsStore(context: Context) {
visibleFields = prefs.getStringSet("visible", LogViewSettings().visibleFields.map { it.name }.toSet())!!
.map(LogField::valueOf).toSet(),
order = prefs.getString("order", null)?.split(',')?.map(LogField::valueOf) ?: LogField.entries,
timestampZone = TimestampZone.valueOf(prefs.getString("zone", TimestampZone.LOCAL_NOW.name)!!),
timestampZone = TimestampZone.valueOf(prefs.getString("zone", TimestampZone.EVENT_LOCAL.name)!!),
timestampDateFormat = TimestampDateFormat.valueOf(prefs.getString("date_format", TimestampDateFormat.SYSTEM_DEFAULT.name)!!),
timestampClockFormat = TimestampClockFormat.valueOf(prefs.getString("clock_format", TimestampClockFormat.SYSTEM_DEFAULT.name)!!),
visibleEvents = prefs.getStringSet("visible_events", DisplayEvent.entries.map { it.name }.toSet())!!
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/dropdown_popup_background" />
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="@color/dropdown_popup_outline" />
</shape>
@@ -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>
@@ -39,7 +39,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Storage limits" />
android:text="Storage limits (MiB)" />
<EditText
android:id="@+id/log_limit_mib"
+6
View File
@@ -9,4 +9,10 @@
<color name="on_surface">#E3E2E9</color>
<color name="window_background">#121318</color>
<color name="log_row_separator">#8F8D96</color>
<color name="dropdown_popup_background">#25262D</color>
<color name="dropdown_popup_outline">#C7C5CF</color>
<color name="app_filter_inherited_background">#2D2E34</color>
<color name="app_filter_inherited_icon">#000000</color>
<color name="app_filter_override_background">#4A4B53</color>
<color name="app_filter_override_icon">#B8CCFF</color>
</resources>
+6
View File
@@ -9,4 +9,10 @@
<color name="on_surface">#1B1B21</color>
<color name="window_background">#FBF8FF</color>
<color name="log_row_separator">#A5A2AB</color>
<color name="dropdown_popup_background">#FFFFFF</color>
<color name="dropdown_popup_outline">#64646C</color>
<color name="app_filter_inherited_background">#DFDFE5</color>
<color name="app_filter_inherited_icon">#FFFFFF</color>
<color name="app_filter_override_background">#D0D0D8</color>
<color name="app_filter_override_icon">#34588E</color>
</resources>
@@ -46,7 +46,8 @@ class MainActivity : AppCompatActivity() {
when (action) {
"post_text" -> postText("Text message")
"edit_text" -> postText("Edited message")
"messaging" -> postMessaging()
"messaging" -> postMessaging(updated = false)
"messaging_update" -> postMessaging(updated = true)
"image" -> postImage()
"dismissible" -> postDismissible()
"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 postMessaging() {
private fun postMessaging(updated: Boolean) {
val me = Person.Builder().setName("Me").build()
val alice = Person.Builder().setName("Alice").build()
val style = Notification.MessagingStyle(me)
.addMessage("Hello from Alice", 1, alice)
.addMessage("A reply from me", 2, null as Person?)
val style = Notification.MessagingStyle(me).addMessage("Hello from Alice", 1, alice)
if (updated) {
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())
}
@@ -126,6 +129,7 @@ class MainActivity : AppCompatActivity() {
const val GROUP_KEY = "helper-group"
val ACTIONS = listOf(
"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 very long text" to "very_long_text",
"Post inbox" to "inbox", "Post progress" to "progress", "Update progress" to "progress_update",
+1 -1
View File
@@ -1 +1 @@
version=0.2
version=1.1