Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9832b80e64 | ||
|
|
96bd5c4d17 | ||
|
|
d85f9050cc | ||
|
|
653080090b | ||
|
|
6eaf2a8784 | ||
|
|
5b8de63aac | ||
|
|
05d682601a | ||
|
|
02b4862294 | ||
|
|
bb822736ab | ||
|
|
591527750d | ||
|
|
5f6f169d3e |
@@ -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,
|
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,
|
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.
|
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
|
## Privacy
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ 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 androidx.lifecycle.lifecycleScope
|
||||||
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
|
||||||
import se.ajpanton.notificationlog.capture.SeenApps
|
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.contentDescription = "Edit logging events for ${app.label}"
|
||||||
holder.filter.imageTintList = ColorStateList.valueOf(
|
val colors = if (app.hasEventOverride) {
|
||||||
MaterialColors.getColor(
|
R.color.app_filter_override_background to R.color.app_filter_override_icon
|
||||||
holder.filter,
|
} else {
|
||||||
if (app.hasEventOverride) com.google.android.material.R.attr.colorPrimary
|
R.color.app_filter_inherited_background to R.color.app_filter_inherited_icon
|
||||||
else com.google.android.material.R.attr.colorOnSurface,
|
}
|
||||||
),
|
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) }
|
holder.filter.setOnClickListener { onEventFilterClicked(app) }
|
||||||
}
|
}
|
||||||
if (holder is SectionTitleHolder && item is AppListItem.SectionTitle) {
|
if (holder is SectionTitleHolder && item is AppListItem.SectionTitle) {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class LogDisplayFragment : Fragment(R.layout.fragment_log_display) {
|
|||||||
): LinearLayout = LinearLayout(requireContext()).apply {
|
): LinearLayout = LinearLayout(requireContext()).apply {
|
||||||
orientation = LinearLayout.VERTICAL
|
orientation = LinearLayout.VERTICAL
|
||||||
setPadding(dp(TIMESTAMP_CONTROLS_INDENT_DP), 0, 0, 0)
|
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])
|
val updated = currentSettings().copy(timestampZone = TimestampZone.entries[position])
|
||||||
saveSettings(updated)
|
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) {
|
private fun LinearLayout.addDropdown(label: String, values: List<String>, selected: Int, onSelected: (Int) -> Unit) {
|
||||||
addView(TextView(context).apply { text = label })
|
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)
|
adapter = ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, values)
|
||||||
setSelection(selected)
|
setSelection(selected)
|
||||||
onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener {
|
onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener {
|
||||||
|
|||||||
@@ -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,21 +78,34 @@ 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") { _, _ ->
|
||||||
|
clearLogs()
|
||||||
|
}
|
||||||
|
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) {
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
EncryptedNotificationLogStore(requireContext().applicationContext).clear()
|
EncryptedNotificationLogStore(appContext).clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun chooseExportFormat() {
|
private fun chooseExportFormat() {
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
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]
|
pendingExport = ExportFormat.entries[which]
|
||||||
createDocument.launch("notification-log.${pendingExport!!.extension}")
|
createDocument.launch("notification-log.${pendingExport!!.extension}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
val skipFilteredPage = appFilterStore.selectedPackages().isNotEmpty() &&
|
||||||
|
visibleAfter == visibleBefore && !noMoreRows
|
||||||
loading = false
|
loading = false
|
||||||
binding?.logsRefresh?.isRefreshing = 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,11 +56,13 @@ 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>()
|
||||||
|
listOf(Notification.EXTRA_HISTORIC_MESSAGES, Notification.EXTRA_MESSAGES).forEach { key ->
|
||||||
Notification.MessagingStyle.Message.getMessagesFromBundleArray(
|
Notification.MessagingStyle.Message.getMessagesFromBundleArray(
|
||||||
extras.getParcelableArray(Notification.EXTRA_MESSAGES, Bundle::class.java),
|
extras.getParcelableArray(key, Bundle::class.java),
|
||||||
).forEach { message ->
|
).forEach { message ->
|
||||||
listOfNotNull(message.senderPerson?.name, message.text).joinToString(": ").addTo(parts)
|
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)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package se.ajpanton.notificationlog.settings
|
package se.ajpanton.notificationlog.settings
|
||||||
|
|
||||||
enum class LogField { TIMESTAMP, APP_NAME, PACKAGE_NAME, ACTION, CONTENTS }
|
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 TimestampDateFormat { SYSTEM_DEFAULT, YEAR_MONTH_DAY, DAY_MONTH_YEAR, MONTH_DAY_YEAR }
|
||||||
enum class TimestampClockFormat { SYSTEM_DEFAULT, HOUR_24, HOUR_12 }
|
enum class TimestampClockFormat { SYSTEM_DEFAULT, HOUR_24, HOUR_12 }
|
||||||
enum class DisplayEvent { APPEARING, DISAPPEARING, EDITS }
|
enum class DisplayEvent { APPEARING, DISAPPEARING, EDITS }
|
||||||
@@ -9,7 +9,7 @@ enum class DisplayEvent { APPEARING, DISAPPEARING, EDITS }
|
|||||||
data class LogViewSettings(
|
data class LogViewSettings(
|
||||||
val visibleFields: Set<LogField> = setOf(LogField.TIMESTAMP, LogField.APP_NAME, LogField.ACTION, LogField.CONTENTS),
|
val visibleFields: Set<LogField> = setOf(LogField.TIMESTAMP, LogField.APP_NAME, LogField.ACTION, LogField.CONTENTS),
|
||||||
val order: List<LogField> = LogField.entries,
|
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 timestampDateFormat: TimestampDateFormat = TimestampDateFormat.SYSTEM_DEFAULT,
|
||||||
val timestampClockFormat: TimestampClockFormat = TimestampClockFormat.SYSTEM_DEFAULT,
|
val timestampClockFormat: TimestampClockFormat = TimestampClockFormat.SYSTEM_DEFAULT,
|
||||||
val visibleEvents: Set<DisplayEvent> = DisplayEvent.entries.toSet(),
|
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())!!
|
visibleFields = prefs.getStringSet("visible", LogViewSettings().visibleFields.map { it.name }.toSet())!!
|
||||||
.map(LogField::valueOf).toSet(),
|
.map(LogField::valueOf).toSet(),
|
||||||
order = prefs.getString("order", null)?.split(',')?.map(LogField::valueOf) ?: LogField.entries,
|
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)!!),
|
timestampDateFormat = TimestampDateFormat.valueOf(prefs.getString("date_format", TimestampDateFormat.SYSTEM_DEFAULT.name)!!),
|
||||||
timestampClockFormat = TimestampClockFormat.valueOf(prefs.getString("clock_format", TimestampClockFormat.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())!!
|
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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="24dp"
|
||||||
android:text="Storage limits" />
|
android:text="Storage limits (MiB)" />
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/log_limit_mib"
|
android:id="@+id/log_limit_mib"
|
||||||
|
|||||||
@@ -9,4 +9,10 @@
|
|||||||
<color name="on_surface">#E3E2E9</color>
|
<color name="on_surface">#E3E2E9</color>
|
||||||
<color name="window_background">#121318</color>
|
<color name="window_background">#121318</color>
|
||||||
<color name="log_row_separator">#8F8D96</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>
|
</resources>
|
||||||
|
|||||||
@@ -9,4 +9,10 @@
|
|||||||
<color name="on_surface">#1B1B21</color>
|
<color name="on_surface">#1B1B21</color>
|
||||||
<color name="window_background">#FBF8FF</color>
|
<color name="window_background">#FBF8FF</color>
|
||||||
<color name="log_row_separator">#A5A2AB</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>
|
</resources>
|
||||||
|
|||||||
@@ -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
@@ -1 +1 @@
|
|||||||
version=0.2
|
version=1.1
|
||||||
|
|||||||
Reference in New Issue
Block a user