Simplify and organize settings pages
This commit is contained in:
@@ -11,7 +11,6 @@ import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -22,24 +21,20 @@ import se.ajpanton.notificationlog.settings.AppListItem
|
||||
import se.ajpanton.notificationlog.settings.AppListOrdering
|
||||
import se.ajpanton.notificationlog.settings.AppRuleMode
|
||||
import se.ajpanton.notificationlog.settings.ListedApp
|
||||
import se.ajpanton.notificationlog.settings.LoggingRule
|
||||
import se.ajpanton.notificationlog.settings.LoggingRuleStore
|
||||
import se.ajpanton.notificationlog.settings.LoggingType
|
||||
import se.ajpanton.notificationlog.settings.AppFilterSettings
|
||||
import se.ajpanton.notificationlog.settings.AppFilterSettingsStore
|
||||
|
||||
class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) {
|
||||
class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
|
||||
private var binding: FragmentEventSettingsBinding? = null
|
||||
private lateinit var type: LoggingType
|
||||
private lateinit var store: LoggingRuleStore
|
||||
private lateinit var store: AppFilterSettingsStore
|
||||
private lateinit var appAdapter: AppListAdapter
|
||||
private var appLoadGeneration = 0
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = FragmentEventSettingsBinding.bind(view)
|
||||
type = requireArguments().getSerializable(ARG_TYPE, LoggingType::class.java)!!
|
||||
store = LoggingRuleStore(requireContext())
|
||||
store = AppFilterSettingsStore(requireContext())
|
||||
appAdapter = AppListAdapter(::setPackageSelected)
|
||||
binding!!.pageTitle.setText(requireArguments().getInt(ARG_TITLE))
|
||||
binding!!.appList.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding!!.appList.adapter = appAdapter
|
||||
binding!!.appListRefresh.setOnRefreshListener(::reloadAppList)
|
||||
@@ -66,40 +61,32 @@ class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) {
|
||||
}
|
||||
|
||||
private fun bindRule() {
|
||||
val rule = store.ruleFor(type)
|
||||
val rule = store.load()
|
||||
listOf(
|
||||
binding!!.masterToggle,
|
||||
binding!!.appRuleToggle,
|
||||
binding!!.onlySeenToggle,
|
||||
binding!!.seenFirstToggle,
|
||||
binding!!.routineUpdatesToggle,
|
||||
).forEach { it.setOnCheckedChangeListener(null) }
|
||||
binding!!.masterToggle.isChecked = rule.enabled
|
||||
binding!!.appRuleToggle.isChecked = rule.appRuleMode == AppRuleMode.WHITELIST
|
||||
binding!!.appRuleToggle.isChecked = rule.mode == AppRuleMode.WHITELIST
|
||||
binding!!.onlySeenToggle.isChecked = rule.onlySeenApps
|
||||
binding!!.seenFirstToggle.isChecked = rule.seenAppsFirst
|
||||
binding!!.seenFirstToggle.isEnabled = !rule.onlySeenApps
|
||||
binding!!.routineUpdatesToggle.visibility = if (type == LoggingType.EDITS) View.VISIBLE else View.GONE
|
||||
binding!!.routineUpdatesToggle.isChecked = rule.ignoreRoutineUpdates
|
||||
updateRuleLabels(rule)
|
||||
binding!!.masterToggle.setOnCheckedChangeListener { _, checked -> save { copy(enabled = checked) } }
|
||||
binding!!.appRuleToggle.setOnCheckedChangeListener { _, checked ->
|
||||
save { copy(appRuleMode = if (checked) AppRuleMode.WHITELIST else AppRuleMode.BLACKLIST) }
|
||||
save { copy(mode = if (checked) AppRuleMode.WHITELIST else AppRuleMode.BLACKLIST) }
|
||||
}
|
||||
binding!!.onlySeenToggle.setOnCheckedChangeListener { _, checked -> save { copy(onlySeenApps = checked) } }
|
||||
binding!!.seenFirstToggle.setOnCheckedChangeListener { _, checked -> save { copy(seenAppsFirst = checked) } }
|
||||
binding!!.routineUpdatesToggle.setOnCheckedChangeListener { _, checked -> save { copy(ignoreRoutineUpdates = checked) } }
|
||||
reloadAppList()
|
||||
}
|
||||
|
||||
private fun save(change: LoggingRule.() -> LoggingRule) {
|
||||
store.save(type, store.ruleFor(type).change())
|
||||
private fun save(change: AppFilterSettings.() -> AppFilterSettings) {
|
||||
store.save(store.load().change())
|
||||
bindRule()
|
||||
}
|
||||
|
||||
private fun updateRuleLabels(rule: LoggingRule) {
|
||||
binding!!.masterToggle.text = "Enable logging"
|
||||
binding!!.appRuleToggle.text = modeLabel(rule.appRuleMode)
|
||||
private fun updateRuleLabels(rule: AppFilterSettings) {
|
||||
binding!!.appRuleToggle.text = modeLabel(rule.mode)
|
||||
binding!!.onlySeenToggle.text = "Show only seen apps"
|
||||
binding!!.seenFirstToggle.text = "Show seen apps first"
|
||||
}
|
||||
@@ -122,7 +109,7 @@ class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) {
|
||||
val generation = ++appLoadGeneration
|
||||
currentBinding.appListLoading.visibility = View.VISIBLE
|
||||
currentBinding.appListRefresh.isRefreshing = true
|
||||
val rule = store.ruleFor(type)
|
||||
val rule = store.load()
|
||||
val seen = SeenApps.snapshot()
|
||||
val context = requireContext().applicationContext
|
||||
Thread {
|
||||
@@ -146,11 +133,11 @@ class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) {
|
||||
}
|
||||
|
||||
private fun setPackageSelected(packageName: String, selected: Boolean) {
|
||||
val current = store.ruleFor(type)
|
||||
val current = store.load()
|
||||
val packages = current.selectedPackages.toMutableSet().apply {
|
||||
if (selected) add(packageName) else remove(packageName)
|
||||
}
|
||||
store.save(type, current.copy(selectedPackages = packages))
|
||||
store.save(current.copy(selectedPackages = packages))
|
||||
// Do not reload: an unchecked unseen row remains visible until the next requested refresh.
|
||||
}
|
||||
|
||||
@@ -231,14 +218,6 @@ class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_TITLE = "title"
|
||||
private const val ARG_TYPE = "type"
|
||||
|
||||
fun newInstance(@StringRes title: Int, type: LoggingType) = EventSettingsFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putInt(ARG_TITLE, title)
|
||||
putSerializable(ARG_TYPE, type)
|
||||
}
|
||||
}
|
||||
fun newInstance() = FilterAppsFragment()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package se.ajpanton.notificationlog
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import se.ajpanton.notificationlog.databinding.FragmentFilterLoggingBinding
|
||||
import se.ajpanton.notificationlog.settings.CaptureSettingsStore
|
||||
import se.ajpanton.notificationlog.settings.LoggingRuleStore
|
||||
import se.ajpanton.notificationlog.settings.LoggingType
|
||||
|
||||
class FilterLoggingFragment : Fragment(R.layout.fragment_filter_logging) {
|
||||
private var binding: FragmentFilterLoggingBinding? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding = FragmentFilterLoggingBinding.bind(view)
|
||||
val rules = LoggingRuleStore(requireContext())
|
||||
addToggles(binding!!.eventToggles, listOf(LoggingType.APPEARING, LoggingType.DISAPPEARING, LoggingType.EDITS), rules)
|
||||
addToggles(binding!!.contentToggles, listOf(LoggingType.TEXT_CONTENT, LoggingType.IMAGE_CONTENT), rules)
|
||||
val capture = CaptureSettingsStore(requireContext())
|
||||
binding!!.groupSummaries.isChecked = capture.logGroupSummaries
|
||||
binding!!.groupSummaries.setOnCheckedChangeListener { _, checked -> capture.logGroupSummaries = checked }
|
||||
binding!!.routineUpdates.isChecked = rules.ruleFor(LoggingType.EDITS).ignoreRoutineUpdates
|
||||
binding!!.routineUpdates.setOnCheckedChangeListener { _, checked -> rules.save(LoggingType.EDITS, rules.ruleFor(LoggingType.EDITS).copy(ignoreRoutineUpdates = checked)) }
|
||||
}
|
||||
|
||||
private fun addToggles(
|
||||
container: android.widget.LinearLayout,
|
||||
types: List<LoggingType>,
|
||||
rules: LoggingRuleStore,
|
||||
) = types.forEach { type ->
|
||||
container.addView(SwitchMaterial(requireContext()).apply {
|
||||
text = type.label()
|
||||
isChecked = rules.ruleFor(type).enabled
|
||||
setOnCheckedChangeListener { _, checked ->
|
||||
rules.save(type, rules.ruleFor(type).copy(enabled = checked))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDestroyView() { binding = null; super.onDestroyView() }
|
||||
|
||||
private fun LoggingType.label() = when (this) {
|
||||
LoggingType.APPEARING -> "Appearing"
|
||||
LoggingType.DISAPPEARING -> "Disappearing"
|
||||
LoggingType.EDITS -> "Edits"
|
||||
LoggingType.TEXT_CONTENT -> "Text content"
|
||||
LoggingType.IMAGE_CONTENT -> "Image content"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package se.ajpanton.notificationlog
|
||||
|
||||
import android.content.ClipData
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import se.ajpanton.notificationlog.databinding.FragmentLogDisplayBinding
|
||||
import se.ajpanton.notificationlog.settings.DisplayEvent
|
||||
import se.ajpanton.notificationlog.settings.LogField
|
||||
import se.ajpanton.notificationlog.settings.LogViewSettingsStore
|
||||
import se.ajpanton.notificationlog.settings.TimestampZone
|
||||
|
||||
class LogDisplayFragment : Fragment(R.layout.fragment_log_display) {
|
||||
private var binding: FragmentLogDisplayBinding? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding = FragmentLogDisplayBinding.bind(view)
|
||||
val store = LogViewSettingsStore(requireContext())
|
||||
var settings = store.load()
|
||||
|
||||
DisplayEvent.entries.forEach { event ->
|
||||
binding!!.eventToggles.addView(SwitchMaterial(requireContext()).apply {
|
||||
text = event.name.lowercase().replaceFirstChar(Char::uppercase)
|
||||
isChecked = event in settings.visibleEvents
|
||||
setOnCheckedChangeListener { _, checked ->
|
||||
settings = settings.copy(
|
||||
visibleEvents = settings.visibleEvents.toMutableSet().apply {
|
||||
if (checked) add(event) else remove(event)
|
||||
},
|
||||
)
|
||||
store.save(settings)
|
||||
}
|
||||
})
|
||||
}
|
||||
settings.order.forEach { field ->
|
||||
binding!!.fieldToggles.addView(SwitchMaterial(requireContext()).apply {
|
||||
text = field.label()
|
||||
isChecked = field in settings.visibleFields
|
||||
tag = field
|
||||
setOnLongClickListener {
|
||||
startDragAndDrop(
|
||||
ClipData.newPlainText("field", field.name),
|
||||
View.DragShadowBuilder(this),
|
||||
this,
|
||||
0,
|
||||
)
|
||||
true
|
||||
}
|
||||
setOnCheckedChangeListener { _, checked ->
|
||||
settings = settings.copy(
|
||||
visibleFields = settings.visibleFields.toMutableSet().apply {
|
||||
if (checked) add(field) else remove(field)
|
||||
},
|
||||
)
|
||||
store.save(settings)
|
||||
}
|
||||
})
|
||||
}
|
||||
binding!!.fieldToggles.setOnDragListener { _, event ->
|
||||
if (event.action != android.view.DragEvent.ACTION_DROP) return@setOnDragListener true
|
||||
val source = event.localState as? View ?: return@setOnDragListener false
|
||||
val box = binding!!.fieldToggles
|
||||
val index = (0 until box.childCount).firstOrNull { event.y < box.getChildAt(it).bottom } ?: box.childCount
|
||||
box.removeView(source)
|
||||
box.addView(source, index.coerceAtMost(box.childCount))
|
||||
settings = settings.copy(order = (0 until box.childCount).map { box.getChildAt(it).tag as LogField })
|
||||
store.save(settings)
|
||||
true
|
||||
}
|
||||
binding!!.timezone.adapter = ArrayAdapter(
|
||||
requireContext(),
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
listOf("Current local timezone", "UTC", "Local timezone when event happened"),
|
||||
)
|
||||
binding!!.timezone.setSelection(TimestampZone.entries.indexOf(settings.timestampZone))
|
||||
binding!!.timezone.onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener {
|
||||
override fun onNothingSelected(parent: android.widget.AdapterView<*>?) = Unit
|
||||
|
||||
override fun onItemSelected(
|
||||
parent: android.widget.AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long,
|
||||
) {
|
||||
settings = settings.copy(timestampZone = TimestampZone.entries[position])
|
||||
store.save(settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() { binding = null; super.onDestroyView() }
|
||||
|
||||
private fun LogField.label() = when (this) {
|
||||
LogField.TIMESTAMP -> "Timestamp"
|
||||
LogField.APP_NAME -> "App name"
|
||||
LogField.PACKAGE_NAME -> "Package name"
|
||||
LogField.ACTION -> "What the app did"
|
||||
LogField.CONTENTS -> "Contents of the notification"
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import se.ajpanton.notificationlog.databinding.ActivityMainBinding
|
||||
import se.ajpanton.notificationlog.settings.LoggingType
|
||||
import se.ajpanton.notificationlog.settings.AppLockStore
|
||||
|
||||
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
|
||||
@@ -137,7 +136,9 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
||||
when {
|
||||
page == Page.VIEW_LOGS -> ViewLogsFragment()
|
||||
page == Page.SETTINGS -> SettingsFragment()
|
||||
page.loggingType != null -> EventSettingsFragment.newInstance(page.titleRes, page.loggingType)
|
||||
page == Page.LOG_DISPLAY -> LogDisplayFragment()
|
||||
page == Page.FILTER_LOGGING -> FilterLoggingFragment()
|
||||
page == Page.FILTER_APPS -> FilterAppsFragment.newInstance()
|
||||
else -> PageFragment.newInstance(page.titleRes)
|
||||
},
|
||||
)
|
||||
@@ -287,15 +288,12 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
||||
private enum class Page(
|
||||
val menuId: Int,
|
||||
val titleRes: Int,
|
||||
val loggingType: LoggingType? = null,
|
||||
) {
|
||||
VIEW_LOGS(R.id.nav_view_logs, R.string.page_view_logs),
|
||||
SETTINGS(R.id.nav_settings, R.string.page_settings),
|
||||
APPEARING(R.id.nav_appearing, R.string.page_appearing, LoggingType.APPEARING),
|
||||
DISAPPEARING(R.id.nav_disappearing, R.string.page_disappearing, LoggingType.DISAPPEARING),
|
||||
TEXT_CONTENT(R.id.nav_text_content, R.string.page_text_content, LoggingType.TEXT_CONTENT),
|
||||
IMAGE_CONTENT(R.id.nav_image_content, R.string.page_image_content, LoggingType.IMAGE_CONTENT),
|
||||
EDITS(R.id.nav_edits, R.string.page_edits, LoggingType.EDITS),
|
||||
LOG_DISPLAY(R.id.nav_log_display, R.string.page_log_display),
|
||||
FILTER_LOGGING(R.id.nav_filter_logging, R.string.page_filter_logging),
|
||||
FILTER_APPS(R.id.nav_filter_apps, R.string.page_filter_apps),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
package se.ajpanton.notificationlog
|
||||
|
||||
import android.os.Bundle
|
||||
import android.content.ClipData
|
||||
import android.net.Uri
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.PopupWindow
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import se.ajpanton.notificationlog.databinding.FragmentSettingsBinding
|
||||
import se.ajpanton.notificationlog.settings.LogField
|
||||
import se.ajpanton.notificationlog.settings.LogViewSettingsStore
|
||||
import se.ajpanton.notificationlog.settings.TimestampZone
|
||||
import se.ajpanton.notificationlog.settings.LoggingType
|
||||
import se.ajpanton.notificationlog.settings.LoggingRuleStore
|
||||
import se.ajpanton.notificationlog.settings.AppLockStore
|
||||
import se.ajpanton.notificationlog.settings.CaptureSettingsStore
|
||||
import se.ajpanton.notificationlog.data.EncryptedNotificationLogStore
|
||||
import se.ajpanton.notificationlog.data.EncryptedImageStore
|
||||
import se.ajpanton.notificationlog.capture.NotificationCaptureService
|
||||
@@ -33,7 +20,6 @@ import java.util.zip.ZipOutputStream
|
||||
|
||||
class SettingsFragment : Fragment(R.layout.fragment_settings) {
|
||||
private var binding: FragmentSettingsBinding? = null
|
||||
private var selectedCopyTargets = emptySet<LoggingType>()
|
||||
private var pendingExport: ExportFormat? = null
|
||||
private val createDocument = registerForActivityResult(androidx.activity.result.contract.ActivityResultContracts.CreateDocument("application/octet-stream")) { uri ->
|
||||
uri?.let(::writeExport)
|
||||
@@ -41,55 +27,6 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = FragmentSettingsBinding.bind(view)
|
||||
val store = LogViewSettingsStore(requireContext())
|
||||
var settings = store.load()
|
||||
settings.order.forEach { field ->
|
||||
binding!!.fieldToggles.addView(SwitchMaterial(requireContext()).apply {
|
||||
text = when (field) {
|
||||
LogField.TIMESTAMP -> "Timestamp"; LogField.APP_NAME -> "App name"
|
||||
LogField.PACKAGE_NAME -> "Package name"; LogField.ACTION -> "What the app did"
|
||||
LogField.CONTENTS -> "Contents of the notification"
|
||||
}
|
||||
isChecked = field in settings.visibleFields
|
||||
tag = field
|
||||
setOnLongClickListener {
|
||||
startDragAndDrop(ClipData.newPlainText("field", field.name), View.DragShadowBuilder(this), this, 0)
|
||||
true
|
||||
}
|
||||
setOnCheckedChangeListener { _, checked ->
|
||||
settings = settings.copy(visibleFields = settings.visibleFields.toMutableSet().apply {
|
||||
if (checked) add(field) else remove(field)
|
||||
})
|
||||
store.save(settings)
|
||||
}
|
||||
})
|
||||
}
|
||||
binding!!.fieldToggles.setOnDragListener { _, event ->
|
||||
if (event.action != android.view.DragEvent.ACTION_DROP) return@setOnDragListener true
|
||||
val source = event.localState as? View ?: return@setOnDragListener false
|
||||
val container = binding!!.fieldToggles
|
||||
val index = (0 until container.childCount).firstOrNull { event.y < container.getChildAt(it).bottom } ?: container.childCount
|
||||
container.removeView(source); container.addView(source, index.coerceAtMost(container.childCount))
|
||||
settings = settings.copy(order = (0 until container.childCount).map { container.getChildAt(it).tag as LogField })
|
||||
store.save(settings)
|
||||
true
|
||||
}
|
||||
val labels = listOf("Current local timezone", "UTC", "Local timezone when event happened")
|
||||
binding!!.timezone.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, labels)
|
||||
binding!!.timezone.setSelection(TimestampZone.entries.indexOf(settings.timestampZone))
|
||||
binding!!.timezone.onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener {
|
||||
override fun onNothingSelected(parent: android.widget.AdapterView<*>?) = Unit
|
||||
override fun onItemSelected(parent: android.widget.AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
settings = settings.copy(timestampZone = TimestampZone.entries[position]); store.save(settings)
|
||||
}
|
||||
}
|
||||
setupCopyControls()
|
||||
setupEventMasterToggles()
|
||||
val captureSettings = CaptureSettingsStore(requireContext())
|
||||
binding!!.groupSummaries.isChecked = captureSettings.logGroupSummaries
|
||||
binding!!.groupSummaries.setOnCheckedChangeListener { _, checked ->
|
||||
captureSettings.logGroupSummaries = checked
|
||||
}
|
||||
binding!!.exportLogs.setOnClickListener { confirmExport() }
|
||||
binding!!.clearLogs.setOnClickListener { confirmClearLogs() }
|
||||
binding!!.notificationAccess.setOnClickListener { startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)) }
|
||||
@@ -101,114 +38,9 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
refreshEventMasterToggles()
|
||||
binding?.notificationAccess?.text = if (isListenerEnabled()) "Notification access enabled" else "Enable notification access"
|
||||
}
|
||||
|
||||
private fun setupEventMasterToggles() {
|
||||
val container = binding!!.eventMasterToggles
|
||||
LoggingType.entries.forEach { type ->
|
||||
container.addView(SwitchMaterial(requireContext()).apply {
|
||||
text = "${type.label()} logging"
|
||||
tag = type
|
||||
setOnCheckedChangeListener { _, enabled ->
|
||||
LoggingRuleStore(requireContext()).save(type, LoggingRuleStore(requireContext()).ruleFor(type).copy(enabled = enabled))
|
||||
}
|
||||
})
|
||||
}
|
||||
refreshEventMasterToggles()
|
||||
}
|
||||
|
||||
private fun refreshEventMasterToggles() {
|
||||
val store = LoggingRuleStore(requireContext())
|
||||
val container = binding?.eventMasterToggles ?: return
|
||||
for (index in 0 until container.childCount) {
|
||||
val toggle = container.getChildAt(index) as SwitchMaterial
|
||||
toggle.setOnCheckedChangeListener(null)
|
||||
toggle.isChecked = store.ruleFor(toggle.tag as LoggingType).enabled
|
||||
toggle.setOnCheckedChangeListener { _, enabled ->
|
||||
val type = toggle.tag as LoggingType
|
||||
store.save(type, store.ruleFor(type).copy(enabled = enabled))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCopyControls() {
|
||||
val labels = listOf("Source") + LoggingType.entries.map { it.label() }
|
||||
binding!!.copyFrom.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, labels)
|
||||
binding!!.copyFrom.onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener {
|
||||
override fun onNothingSelected(parent: android.widget.AdapterView<*>?) = Unit
|
||||
override fun onItemSelected(parent: android.widget.AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
selectedCopyTargets = emptySet()
|
||||
binding!!.copyTo.isEnabled = position > 0
|
||||
binding!!.copyTo.text = "Destination"
|
||||
updateCopyControls()
|
||||
}
|
||||
}
|
||||
binding!!.copyTo.setOnClickListener { showDestinationDropdown(LoggingType.entries[binding!!.copyFrom.selectedItemPosition - 1]) }
|
||||
binding!!.copyEventSettings.setOnClickListener {
|
||||
LoggingRuleStore(requireContext()).copy(LoggingType.entries[binding!!.copyFrom.selectedItemPosition - 1], selectedCopyTargets)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDestinationDropdown(source: LoggingType) {
|
||||
val targets = LoggingType.entries.filterNot { it == source }
|
||||
val popupContent = LinearLayout(requireContext()).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(dp(8), dp(8), dp(8), dp(8))
|
||||
}
|
||||
val popup = PopupWindow(popupContent, binding!!.copyTo.width.coerceAtLeast(dp(200)), LinearLayout.LayoutParams.WRAP_CONTENT, true).apply {
|
||||
isOutsideTouchable = true
|
||||
elevation = dp(8).toFloat()
|
||||
setBackgroundDrawable(ColorDrawable(MaterialColors.getColor(requireContext(), com.google.android.material.R.attr.colorSurface, Color.WHITE)))
|
||||
}
|
||||
val boxes = mutableListOf<CheckBox>()
|
||||
popupContent.addView(android.widget.Button(requireContext()).apply {
|
||||
text = "All"
|
||||
setOnClickListener {
|
||||
val selectAll = selectedCopyTargets.size != targets.size
|
||||
selectedCopyTargets = if (selectAll) targets.toSet() else emptySet()
|
||||
boxes.forEachIndexed { index, box -> box.isChecked = targets[index] in selectedCopyTargets }
|
||||
updateCopyControls()
|
||||
}
|
||||
})
|
||||
popupContent.addView(View(requireContext()).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 1).apply {
|
||||
setMargins(0, dp(4), 0, dp(4))
|
||||
}
|
||||
setBackgroundColor(0x33000000)
|
||||
})
|
||||
targets.forEach { target ->
|
||||
val box = CheckBox(requireContext()).apply {
|
||||
text = target.label()
|
||||
isChecked = target in selectedCopyTargets
|
||||
setOnCheckedChangeListener { _, checked ->
|
||||
selectedCopyTargets = selectedCopyTargets.toMutableSet().apply {
|
||||
if (checked) add(target) else remove(target)
|
||||
}
|
||||
updateCopyControls()
|
||||
}
|
||||
}
|
||||
boxes += box
|
||||
popupContent.addView(box)
|
||||
}
|
||||
popup.showAsDropDown(binding!!.copyTo)
|
||||
}
|
||||
|
||||
private fun updateCopyControls() {
|
||||
val currentBinding = binding ?: return
|
||||
val sourceChosen = currentBinding.copyFrom.selectedItemPosition > 0
|
||||
currentBinding.copyTo.isEnabled = sourceChosen
|
||||
currentBinding.copyTo.text = destinationLabel()
|
||||
currentBinding.copyEventSettings.isEnabled = sourceChosen && selectedCopyTargets.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun destinationLabel(): String = when (selectedCopyTargets.size) {
|
||||
0 -> "Destination"
|
||||
1 -> selectedCopyTargets.first().label()
|
||||
else -> "${selectedCopyTargets.sortedBy { it.ordinal }.first().label()}..."
|
||||
}
|
||||
|
||||
private fun isListenerEnabled(): Boolean = requireContext().getSystemService(android.app.NotificationManager::class.java)
|
||||
.isNotificationListenerAccessGranted(ComponentName(requireContext(), NotificationCaptureService::class.java))
|
||||
|
||||
@@ -271,9 +103,5 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun LoggingType.label() = name.lowercase().replace('_', ' ').replaceFirstChar(Char::uppercase)
|
||||
|
||||
private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
|
||||
|
||||
private enum class ExportFormat(val extension: String) { CSV("csv"), FORMATTED("txt"), HTML_ZIP("zip") }
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import se.ajpanton.notificationlog.settings.LogField
|
||||
import se.ajpanton.notificationlog.settings.LogViewSettings
|
||||
import se.ajpanton.notificationlog.settings.LogViewSettingsStore
|
||||
import se.ajpanton.notificationlog.settings.TimestampZone
|
||||
import se.ajpanton.notificationlog.settings.DisplayEvent
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -67,7 +68,8 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
||||
view.logRows.removeAllViews()
|
||||
view.emptyView.visibility = if (rows.isEmpty()) View.VISIBLE else View.GONE
|
||||
val settings = LogViewSettingsStore(requireContext()).load()
|
||||
rows.forEach { row -> view.logRows.addView(logRowView(row, settings)) }
|
||||
rows.filter { eventFor(it.entry.action) in settings.visibleEvents }
|
||||
.forEach { row -> view.logRows.addView(logRowView(row, settings)) }
|
||||
}
|
||||
|
||||
private fun logRowView(row: LogRow, settings: LogViewSettings): View = LinearLayout(requireContext()).apply {
|
||||
@@ -201,6 +203,12 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
||||
else -> action.name.lowercase().replace('_', ' ').replaceFirstChar(Char::uppercase)
|
||||
}
|
||||
|
||||
private fun eventFor(action: NotificationAction): DisplayEvent = when (action) {
|
||||
NotificationAction.APPEARED, NotificationAction.ALREADY_ACTIVE -> DisplayEvent.APPEARING
|
||||
NotificationAction.EDITED -> DisplayEvent.EDITS
|
||||
else -> DisplayEvent.DISAPPEARING
|
||||
}
|
||||
|
||||
private fun timestamp(value: Long, zone: TimestampZone, eventTimeZoneId: String?): String = DateFormat.getDateTimeInstance().apply {
|
||||
timeZone = when (zone) {
|
||||
TimestampZone.UTC -> java.util.TimeZone.getTimeZone("UTC")
|
||||
|
||||
+10
-7
@@ -13,6 +13,7 @@ import se.ajpanton.notificationlog.settings.LoggingRuleStore
|
||||
import se.ajpanton.notificationlog.settings.LoggingType
|
||||
import se.ajpanton.notificationlog.settings.NotificationRuleEvaluator
|
||||
import se.ajpanton.notificationlog.settings.CaptureSettingsStore
|
||||
import se.ajpanton.notificationlog.settings.AppFilterSettingsStore
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@@ -22,6 +23,7 @@ class NotificationCaptureService : NotificationListenerService() {
|
||||
private lateinit var writeExecutor: ExecutorService
|
||||
private lateinit var ruleStore: LoggingRuleStore
|
||||
private lateinit var captureSettings: CaptureSettingsStore
|
||||
private lateinit var appFilterStore: AppFilterSettingsStore
|
||||
private lateinit var imageStore: EncryptedImageStore
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -29,6 +31,7 @@ class NotificationCaptureService : NotificationListenerService() {
|
||||
logStore = EncryptedNotificationLogStore(this)
|
||||
ruleStore = LoggingRuleStore(this)
|
||||
captureSettings = CaptureSettingsStore(this)
|
||||
appFilterStore = AppFilterSettingsStore(this)
|
||||
imageStore = EncryptedImageStore(this)
|
||||
writeExecutor = Executors.newSingleThreadExecutor { runnable ->
|
||||
Thread(runnable, "notification-log-writer")
|
||||
@@ -80,10 +83,9 @@ class NotificationCaptureService : NotificationListenerService() {
|
||||
previousSnapshot: NotificationSnapshot? = null,
|
||||
) {
|
||||
if (!GroupSummaryPolicy.shouldLog(snapshot, captureSettings.logGroupSummaries)) return
|
||||
if (!NotificationRuleEvaluator.allows(ruleStore.ruleFor(loggingType), snapshot.packageName)) return
|
||||
if (!allows(loggingType, snapshot.packageName)) return
|
||||
val appName = appName(snapshot.packageName)
|
||||
val retainImage = includeContents && snapshot.imageBytes != null &&
|
||||
NotificationRuleEvaluator.allows(ruleStore.ruleFor(LoggingType.IMAGE_CONTENT), snapshot.packageName)
|
||||
val retainImage = includeContents && snapshot.imageBytes != null && allows(LoggingType.IMAGE_CONTENT, snapshot.packageName)
|
||||
val entry = NotificationLogEntry(
|
||||
recordedAtEpochMillis = System.currentTimeMillis(),
|
||||
eventTimeZoneId = java.util.TimeZone.getDefault().id,
|
||||
@@ -115,17 +117,18 @@ class NotificationCaptureService : NotificationListenerService() {
|
||||
|
||||
private fun visibleContents(snapshot: NotificationSnapshot): String? {
|
||||
val text = snapshot.textContents?.takeIf {
|
||||
NotificationRuleEvaluator.allows(ruleStore.ruleFor(LoggingType.TEXT_CONTENT), snapshot.packageName)
|
||||
allows(LoggingType.TEXT_CONTENT, snapshot.packageName)
|
||||
}
|
||||
val image = snapshot.hasImage && NotificationRuleEvaluator.allows(
|
||||
ruleStore.ruleFor(LoggingType.IMAGE_CONTENT), snapshot.packageName,
|
||||
)
|
||||
val image = snapshot.hasImage && allows(LoggingType.IMAGE_CONTENT, snapshot.packageName)
|
||||
return listOfNotNull(text, if (image) "[image]" else null)
|
||||
.joinToString("\n")
|
||||
.take(MAX_CONTENT_CHARACTERS)
|
||||
.ifEmpty { null }
|
||||
}
|
||||
|
||||
private fun allows(type: LoggingType, packageName: String): Boolean =
|
||||
ruleStore.ruleFor(type).enabled && NotificationRuleEvaluator.allows(appFilterStore.load(), packageName)
|
||||
|
||||
private fun appName(packageName: String): String = try {
|
||||
val applicationInfo = packageManager.getApplicationInfo(packageName, 0)
|
||||
packageManager.getApplicationLabel(applicationInfo).toString()
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package se.ajpanton.notificationlog.settings
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
|
||||
data class AppFilterSettings(
|
||||
val mode: AppRuleMode = AppRuleMode.BLACKLIST,
|
||||
val selectedPackages: Set<String> = emptySet(),
|
||||
val onlySeenApps: Boolean = false,
|
||||
val seenAppsFirst: Boolean = true,
|
||||
)
|
||||
|
||||
class AppFilterSettingsStore(context: Context) {
|
||||
private val appContext = context.applicationContext
|
||||
private val preferences = appContext.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
fun load(): AppFilterSettings {
|
||||
migrateLegacyFiltersIfNeeded()
|
||||
return AppFilterSettings(
|
||||
mode = AppRuleMode.valueOf(preferences.getString(MODE, AppRuleMode.BLACKLIST.name)!!),
|
||||
selectedPackages = preferences.getStringSet(SELECTED_PACKAGES, emptySet())?.toSet() ?: emptySet(),
|
||||
onlySeenApps = preferences.getBoolean(ONLY_SEEN_APPS, false),
|
||||
seenAppsFirst = preferences.getBoolean(SEEN_APPS_FIRST, true),
|
||||
)
|
||||
}
|
||||
|
||||
fun save(settings: AppFilterSettings) = preferences.edit {
|
||||
putString(MODE, settings.mode.name)
|
||||
putStringSet(SELECTED_PACKAGES, settings.selectedPackages)
|
||||
putBoolean(ONLY_SEEN_APPS, settings.onlySeenApps)
|
||||
putBoolean(SEEN_APPS_FIRST, settings.seenAppsFirst)
|
||||
}
|
||||
|
||||
private fun migrateLegacyFiltersIfNeeded() {
|
||||
if (preferences.getBoolean(MIGRATED, false)) return
|
||||
val legacy = appContext.getSharedPreferences("logging-rules", Context.MODE_PRIVATE)
|
||||
val blacklistedPackages = LoggingType.entries.flatMap { type ->
|
||||
legacy.getStringSet("${type.name.lowercase()}.selected_packages", emptySet()).orEmpty()
|
||||
.takeIf { legacy.getString("${type.name.lowercase()}.app_rule_mode", AppRuleMode.BLACKLIST.name) == AppRuleMode.BLACKLIST.name }
|
||||
.orEmpty()
|
||||
}.toSet()
|
||||
save(AppFilterSettings(selectedPackages = blacklistedPackages))
|
||||
preferences.edit { putBoolean(MIGRATED, true) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val FILE_NAME = "app-filter-settings"
|
||||
const val MIGRATED = "migrated_from_per_event_filters"
|
||||
const val MODE = "mode"
|
||||
const val SELECTED_PACKAGES = "selected_packages"
|
||||
const val ONLY_SEEN_APPS = "only_seen_apps"
|
||||
const val SEEN_APPS_FIRST = "seen_apps_first"
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ 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 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 visibleEvents: Set<DisplayEvent> = DisplayEvent.entries.toSet(),
|
||||
)
|
||||
|
||||
@@ -10,10 +10,13 @@ class LogViewSettingsStore(context: Context) {
|
||||
.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)!!),
|
||||
visibleEvents = prefs.getStringSet("visible_events", DisplayEvent.entries.map { it.name }.toSet())!!
|
||||
.map(DisplayEvent::valueOf).toSet(),
|
||||
)
|
||||
fun save(settings: LogViewSettings) = prefs.edit {
|
||||
putStringSet("visible", settings.visibleFields.map { it.name }.toSet())
|
||||
putString("order", settings.order.joinToString(",") { it.name })
|
||||
putString("zone", settings.timestampZone.name)
|
||||
putStringSet("visible_events", settings.visibleEvents.map { it.name }.toSet())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,6 @@ class LoggingRuleStore(context: Context) {
|
||||
|
||||
fun ruleFor(type: LoggingType): LoggingRule = LoggingRule(
|
||||
enabled = preferences.getBoolean(key(type, "enabled"), true),
|
||||
appRuleMode = AppRuleMode.valueOf(
|
||||
preferences.getString(key(type, "app_rule_mode"), AppRuleMode.BLACKLIST.name)
|
||||
?: AppRuleMode.BLACKLIST.name,
|
||||
),
|
||||
selectedPackages = preferences.getStringSet(key(type, "selected_packages"), emptySet())?.toSet()
|
||||
?: emptySet(),
|
||||
onlySeenApps = preferences.getBoolean(key(type, "only_seen_apps"), false),
|
||||
seenAppsFirst = preferences.getBoolean(key(type, "seen_apps_first"), true),
|
||||
ignoreRoutineUpdates = preferences.getBoolean(key(type, "ignore_routine_updates"), true),
|
||||
)
|
||||
|
||||
@@ -28,22 +20,8 @@ class LoggingRuleStore(context: Context) {
|
||||
updateListenerComponent()
|
||||
}
|
||||
|
||||
fun copy(from: LoggingType, targets: Set<LoggingType>) {
|
||||
val source = ruleFor(from).copyForTarget()
|
||||
preferences.edit {
|
||||
targets.filterNot { it == from }.forEach { target ->
|
||||
write(target, source.copy(selectedPackages = source.selectedPackages.toSet()))
|
||||
}
|
||||
}
|
||||
updateListenerComponent()
|
||||
}
|
||||
|
||||
private fun android.content.SharedPreferences.Editor.write(type: LoggingType, rule: LoggingRule) {
|
||||
putBoolean(key(type, "enabled"), rule.enabled)
|
||||
putString(key(type, "app_rule_mode"), rule.appRuleMode.name)
|
||||
putStringSet(key(type, "selected_packages"), rule.selectedPackages)
|
||||
putBoolean(key(type, "only_seen_apps"), rule.onlySeenApps)
|
||||
putBoolean(key(type, "seen_apps_first"), rule.seenAppsFirst)
|
||||
putBoolean(key(type, "ignore_routine_updates"), rule.ignoreRoutineUpdates)
|
||||
}
|
||||
|
||||
@@ -57,5 +35,3 @@ class LoggingRuleStore(context: Context) {
|
||||
const val FILE_NAME = "logging-rules"
|
||||
}
|
||||
}
|
||||
|
||||
internal fun LoggingRule.copyForTarget(): LoggingRule = copy(selectedPackages = selectedPackages.toSet())
|
||||
|
||||
@@ -15,9 +15,5 @@ enum class AppRuleMode {
|
||||
|
||||
data class LoggingRule(
|
||||
val enabled: Boolean = true,
|
||||
val appRuleMode: AppRuleMode = AppRuleMode.BLACKLIST,
|
||||
val selectedPackages: Set<String> = emptySet(),
|
||||
val onlySeenApps: Boolean = false,
|
||||
val seenAppsFirst: Boolean = true,
|
||||
val ignoreRoutineUpdates: Boolean = true,
|
||||
)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package se.ajpanton.notificationlog.settings
|
||||
|
||||
object NotificationRuleEvaluator {
|
||||
fun allows(rule: LoggingRule, packageName: String): Boolean {
|
||||
if (!rule.enabled) return false
|
||||
val selected = packageName in rule.selectedPackages
|
||||
return when (rule.appRuleMode) {
|
||||
fun allows(filter: AppFilterSettings, packageName: String): Boolean {
|
||||
val selected = packageName in filter.selectedPackages
|
||||
return when (filter.mode) {
|
||||
AppRuleMode.WHITELIST -> selected
|
||||
AppRuleMode.BLACKLIST -> !selected
|
||||
}
|
||||
|
||||
@@ -5,19 +5,6 @@
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/page_padding">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/page_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceHeadline5" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/master_toggle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="Enable logging" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/app_rule_toggle"
|
||||
android:layout_width="match_parent"
|
||||
@@ -35,12 +22,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Show seen apps first" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/routine_updates_toggle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Ignore routine updates" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="@dimen/page_padding">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Events to log" />
|
||||
<LinearLayout android:id="@+id/event_toggles" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="Content to log" />
|
||||
<LinearLayout android:id="@+id/content_toggles" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="Special filters" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/group_summaries" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Log Android group summaries" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/routine_updates" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Ignore detected routine updates" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="@dimen/page_padding">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Filter events" />
|
||||
<LinearLayout android:id="@+id/event_toggles" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="On each line (long-press and drag to reorder)" />
|
||||
<LinearLayout android:id="@+id/field_toggles" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="Timestamp timezone" />
|
||||
<Spinner android:id="@+id/timezone" android:layout_width="match_parent" android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -9,113 +9,6 @@
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/page_padding">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Settings"
|
||||
android:textAppearance="?attr/textAppearanceHeadline5" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="Event logging" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/group_summaries"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Log Android group summaries" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/event_master_toggles"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="Show on each log line (long-press and drag to reorder)" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/field_toggles"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="Timestamp timezone" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/timezone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="Copy settings between pages" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Copy from" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/copy_from"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="to" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/copy_to"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_weight="1"
|
||||
android:enabled="false"
|
||||
android:gravity="start|center_vertical"
|
||||
android:text="Destination" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/copy_event_settings"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="false"
|
||||
android:text="Copy settings" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="Logs" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/notification_access"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -10,19 +10,13 @@
|
||||
android:id="@+id/nav_settings"
|
||||
android:title="@string/navigation_settings_header" />
|
||||
<item
|
||||
android:id="@+id/nav_appearing"
|
||||
android:title="@string/navigation_indented_appearing" />
|
||||
android:id="@+id/nav_log_display"
|
||||
android:title="@string/navigation_indented_log_display" />
|
||||
<item
|
||||
android:id="@+id/nav_disappearing"
|
||||
android:title="@string/navigation_indented_disappearing" />
|
||||
android:id="@+id/nav_filter_logging"
|
||||
android:title="@string/navigation_indented_filter_logging" />
|
||||
<item
|
||||
android:id="@+id/nav_text_content"
|
||||
android:title="@string/navigation_indented_text_content" />
|
||||
<item
|
||||
android:id="@+id/nav_image_content"
|
||||
android:title="@string/navigation_indented_image_content" />
|
||||
<item
|
||||
android:id="@+id/nav_edits"
|
||||
android:title="@string/navigation_indented_edits" />
|
||||
android:id="@+id/nav_filter_apps"
|
||||
android:title="@string/navigation_indented_filter_apps" />
|
||||
</group>
|
||||
</menu>
|
||||
|
||||
@@ -7,14 +7,10 @@
|
||||
<string name="navigation_settings_header">Settings</string>
|
||||
<string name="page_view_logs">View logs</string>
|
||||
<string name="page_settings">Settings</string>
|
||||
<string name="page_appearing">Appearing</string>
|
||||
<string name="page_disappearing">Disappearing</string>
|
||||
<string name="page_text_content">Text content</string>
|
||||
<string name="page_image_content">Image content</string>
|
||||
<string name="page_edits">Edits</string>
|
||||
<string name="navigation_indented_appearing">    Appearing</string>
|
||||
<string name="navigation_indented_disappearing">    Disappearing</string>
|
||||
<string name="navigation_indented_text_content">    Text content</string>
|
||||
<string name="navigation_indented_image_content">    Image content</string>
|
||||
<string name="navigation_indented_edits">    Edits</string>
|
||||
<string name="page_filter_apps">Filter apps</string>
|
||||
<string name="page_log_display">Log display</string>
|
||||
<string name="page_filter_logging">Filter logging</string>
|
||||
<string name="navigation_indented_log_display">    Log display</string>
|
||||
<string name="navigation_indented_filter_logging">    Filter logging</string>
|
||||
<string name="navigation_indented_filter_apps">    Filter apps</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user