Complete settings controls and refresh behavior
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package se.ajpanton.notificationlog
|
package se.ajpanton.notificationlog
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CheckBox
|
import android.widget.CheckBox
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
@@ -25,11 +26,36 @@ class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) {
|
|||||||
store = LoggingRuleStore(requireContext())
|
store = LoggingRuleStore(requireContext())
|
||||||
binding!!.pageTitle.setText(requireArguments().getInt(ARG_TITLE))
|
binding!!.pageTitle.setText(requireArguments().getInt(ARG_TITLE))
|
||||||
bindRule()
|
bindRule()
|
||||||
binding!!.appListRefresh.setOnRefreshListener { reloadAppList(); binding!!.appListRefresh.isRefreshing = false }
|
binding!!.appListRefresh.setOnRefreshListener(::refreshAppList)
|
||||||
|
var touchStartY = 0f
|
||||||
|
val scroll = binding!!.appScroll
|
||||||
|
scroll.setOnTouchListener { _, event ->
|
||||||
|
when (event.actionMasked) {
|
||||||
|
MotionEvent.ACTION_DOWN -> touchStartY = event.y
|
||||||
|
MotionEvent.ACTION_UP -> {
|
||||||
|
val atBottom = scroll.scrollY >= (scroll.getChildAt(0).height - scroll.height).coerceAtLeast(0)
|
||||||
|
if (atBottom && event.y < touchStartY) {
|
||||||
|
refreshAppList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() { binding = null; super.onDestroyView() }
|
override fun onDestroyView() { binding = null; super.onDestroyView() }
|
||||||
|
|
||||||
|
private fun refreshAppList() {
|
||||||
|
val currentBinding = binding ?: return
|
||||||
|
currentBinding.appListRefresh.isRefreshing = true
|
||||||
|
currentBinding.appListRefresh.post {
|
||||||
|
if (binding === currentBinding) {
|
||||||
|
reloadAppList()
|
||||||
|
currentBinding.appListRefresh.isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun bindRule() {
|
private fun bindRule() {
|
||||||
val rule = store.ruleFor(type)
|
val rule = store.ruleFor(type)
|
||||||
listOf(
|
listOf(
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package se.ajpanton.notificationlog
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
|
import android.net.Uri
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import se.ajpanton.notificationlog.databinding.FragmentSettingsBinding
|
import se.ajpanton.notificationlog.databinding.FragmentSettingsBinding
|
||||||
import se.ajpanton.notificationlog.settings.LogField
|
import se.ajpanton.notificationlog.settings.LogField
|
||||||
import se.ajpanton.notificationlog.settings.LogViewSettingsStore
|
import se.ajpanton.notificationlog.settings.LogViewSettingsStore
|
||||||
@@ -13,11 +15,17 @@ import se.ajpanton.notificationlog.settings.TimestampZone
|
|||||||
import se.ajpanton.notificationlog.settings.LoggingType
|
import se.ajpanton.notificationlog.settings.LoggingType
|
||||||
import se.ajpanton.notificationlog.settings.LoggingRuleStore
|
import se.ajpanton.notificationlog.settings.LoggingRuleStore
|
||||||
import se.ajpanton.notificationlog.settings.AppLockStore
|
import se.ajpanton.notificationlog.settings.AppLockStore
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import se.ajpanton.notificationlog.data.EncryptedNotificationLogStore
|
||||||
|
import se.ajpanton.notificationlog.export.LogExporter
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
class SettingsFragment : Fragment(R.layout.fragment_settings) {
|
class SettingsFragment : Fragment(R.layout.fragment_settings) {
|
||||||
private var binding: FragmentSettingsBinding? = null
|
private var binding: FragmentSettingsBinding? = null
|
||||||
private var selectedCopyTargets = emptySet<LoggingType>()
|
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)
|
||||||
|
}
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
binding = FragmentSettingsBinding.bind(view)
|
binding = FragmentSettingsBinding.bind(view)
|
||||||
@@ -64,19 +72,57 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setupCopyControls()
|
setupCopyControls()
|
||||||
|
setupEventMasterToggles()
|
||||||
|
binding!!.exportLogs.setOnClickListener { chooseExportFormat() }
|
||||||
|
binding!!.clearLogs.setOnClickListener { confirmClearLogs() }
|
||||||
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 -> lockStore.enabled = enabled }
|
||||||
}
|
}
|
||||||
override fun onDestroyView() { binding = null; super.onDestroyView() }
|
override fun onDestroyView() { binding = null; super.onDestroyView() }
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
refreshEventMasterToggles()
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
private fun setupCopyControls() {
|
||||||
val labels = listOf("From") + LoggingType.entries.map { it.label() }
|
val labels = listOf("From") + LoggingType.entries.map { it.label() }
|
||||||
binding!!.copyFrom.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, labels)
|
binding!!.copyFrom.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, labels)
|
||||||
binding!!.copyFrom.onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener {
|
binding!!.copyFrom.onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener {
|
||||||
override fun onNothingSelected(parent: android.widget.AdapterView<*>?) = Unit
|
override fun onNothingSelected(parent: android.widget.AdapterView<*>?) = Unit
|
||||||
override fun onItemSelected(parent: android.widget.AdapterView<*>?, view: View?, position: Int, id: Long) {
|
override fun onItemSelected(parent: android.widget.AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
selectedCopyTargets = emptySet(); binding!!.copyTo.isEnabled = position > 0; updateCopyButton()
|
selectedCopyTargets = emptySet()
|
||||||
|
binding!!.copyTo.isEnabled = position > 0
|
||||||
|
updateCopyButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding!!.copyTo.setOnClickListener { chooseCopyTargets(LoggingType.entries[binding!!.copyFrom.selectedItemPosition - 1]) }
|
binding!!.copyTo.setOnClickListener { chooseCopyTargets(LoggingType.entries[binding!!.copyFrom.selectedItemPosition - 1]) }
|
||||||
@@ -87,17 +133,70 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
|
|||||||
|
|
||||||
private fun chooseCopyTargets(source: LoggingType) {
|
private fun chooseCopyTargets(source: LoggingType) {
|
||||||
val targets = LoggingType.entries.filterNot { it == source }
|
val targets = LoggingType.entries.filterNot { it == source }
|
||||||
val checked = BooleanArray(targets.size)
|
val checked = BooleanArray(targets.size) { targets[it] in selectedCopyTargets }
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle("Copy ${source.label()} settings to")
|
.setTitle("Copy ${source.label()} settings to")
|
||||||
.setMultiChoiceItems(targets.map { it.label() }.toTypedArray(), checked) { _, index, selected -> checked[index] = selected }
|
.setMultiChoiceItems(targets.map { it.label() }.toTypedArray(), checked) { _, index, selected -> checked[index] = selected }
|
||||||
.setNegativeButton("Cancel", null)
|
.setNegativeButton("Cancel", null)
|
||||||
.setPositiveButton("Copy") { _, _ ->
|
.setNeutralButton("All", null)
|
||||||
selectedCopyTargets = targets.filterIndexed { index, _ -> checked[index] }.toSet(); updateCopyButton()
|
.setPositiveButton("Done") { _, _ ->
|
||||||
}.show()
|
selectedCopyTargets = targets.filterIndexed { index, _ -> checked[index] }.toSet()
|
||||||
|
updateCopyButton()
|
||||||
|
}
|
||||||
|
.create()
|
||||||
|
dialog.setOnShowListener {
|
||||||
|
dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
|
||||||
|
val selectAll = checked.any { !it }
|
||||||
|
checked.indices.forEach { index ->
|
||||||
|
checked[index] = selectAll
|
||||||
|
dialog.listView.setItemChecked(index, selectAll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCopyButton() { binding?.copyEventSettings?.isEnabled = selectedCopyTargets.isNotEmpty() }
|
private fun updateCopyButton() { binding?.copyEventSettings?.isEnabled = selectedCopyTargets.isNotEmpty() }
|
||||||
|
|
||||||
|
private fun confirmClearLogs() {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle("Clear logs?")
|
||||||
|
.setMessage("This permanently removes every stored notification log and copied image.")
|
||||||
|
.setNegativeButton("Cancel", null)
|
||||||
|
.setPositiveButton("Clear") { _, _ -> EncryptedNotificationLogStore(requireContext()).clear() }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chooseExportFormat() {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setItems(arrayOf("CSV", "Formatted text", "HTML ZIP")) { _, which ->
|
||||||
|
pendingExport = ExportFormat.entries[which]
|
||||||
|
createDocument.launch("notification-log.${pendingExport!!.extension}")
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeExport(uri: Uri) {
|
||||||
|
val format = pendingExport ?: return
|
||||||
|
val context = requireContext().applicationContext
|
||||||
|
Thread {
|
||||||
|
val entries = EncryptedNotificationLogStore(context).readAll()
|
||||||
|
val settings = LogViewSettingsStore(context).load()
|
||||||
|
context.contentResolver.openOutputStream(uri)?.use { output ->
|
||||||
|
when (format) {
|
||||||
|
ExportFormat.CSV -> output.write(LogExporter.csv(entries, settings).encodeToByteArray())
|
||||||
|
ExportFormat.FORMATTED -> output.write(LogExporter.formatted(entries, settings).encodeToByteArray())
|
||||||
|
ExportFormat.HTML_ZIP -> ZipOutputStream(output).use { zip ->
|
||||||
|
zip.putNextEntry(java.util.zip.ZipEntry("notification-log.html"))
|
||||||
|
zip.write(LogExporter.html(entries, settings).encodeToByteArray())
|
||||||
|
zip.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
private fun LoggingType.label() = name.lowercase().replace('_', ' ').replaceFirstChar(Char::uppercase)
|
private fun LoggingType.label() = name.lowercase().replace('_', ' ').replaceFirstChar(Char::uppercase)
|
||||||
|
|
||||||
|
private enum class ExportFormat(val extension: String) { CSV("csv"), FORMATTED("txt"), HTML_ZIP("zip") }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import java.io.BufferedOutputStream
|
|||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
import java.io.DataOutputStream
|
import java.io.DataOutputStream
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An atomically replaced encrypted event log. It keeps every persisted log field
|
* An atomically replaced encrypted event log. It keeps every persisted log field
|
||||||
@@ -16,6 +17,7 @@ import java.io.FileNotFoundException
|
|||||||
class EncryptedNotificationLogStore(context: Context) {
|
class EncryptedNotificationLogStore(context: Context) {
|
||||||
private val lock = Any()
|
private val lock = Any()
|
||||||
private val file = AtomicFile(context.filesDir.resolve(FILE_NAME))
|
private val file = AtomicFile(context.filesDir.resolve(FILE_NAME))
|
||||||
|
private val imageDirectory = File(context.filesDir, IMAGE_DIRECTORY_NAME)
|
||||||
private val cipher = AesGcmCipher(LogEncryptionKeyProvider().getOrCreate())
|
private val cipher = AesGcmCipher(LogEncryptionKeyProvider().getOrCreate())
|
||||||
|
|
||||||
fun readAll(): List<NotificationLogEntry> = synchronized(lock) {
|
fun readAll(): List<NotificationLogEntry> = synchronized(lock) {
|
||||||
@@ -34,6 +36,7 @@ class EncryptedNotificationLogStore(context: Context) {
|
|||||||
|
|
||||||
fun clear() = synchronized(lock) {
|
fun clear() = synchronized(lock) {
|
||||||
file.delete()
|
file.delete()
|
||||||
|
imageDirectory.listFiles()?.forEach(File::delete)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun write(entries: List<NotificationLogEntry>) {
|
private fun write(entries: List<NotificationLogEntry>) {
|
||||||
@@ -79,5 +82,6 @@ class EncryptedNotificationLogStore(context: Context) {
|
|||||||
const val FILE_VERSION = 1
|
const val FILE_VERSION = 1
|
||||||
const val MAX_IV_BYTES = 32
|
const val MAX_IV_BYTES = 32
|
||||||
const val MAX_CIPHER_TEXT_BYTES = 50 * 1024 * 1024
|
const val MAX_CIPHER_TEXT_BYTES = 50 * 1024 * 1024
|
||||||
|
const val IMAGE_DIRECTORY_NAME = "notification-images"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,20 @@ class LoggingRuleStore(context: Context) {
|
|||||||
|
|
||||||
fun save(type: LoggingType, rule: LoggingRule) {
|
fun save(type: LoggingType, rule: LoggingRule) {
|
||||||
preferences.edit {
|
preferences.edit {
|
||||||
|
write(type, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copy(from: LoggingType, targets: Set<LoggingType>) {
|
||||||
|
val source = ruleFor(from)
|
||||||
|
preferences.edit {
|
||||||
|
targets.filterNot { it == from }.forEach { target ->
|
||||||
|
write(target, source.copy(selectedPackages = source.selectedPackages.toSet()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun android.content.SharedPreferences.Editor.write(type: LoggingType, rule: LoggingRule) {
|
||||||
putBoolean(key(type, "enabled"), rule.enabled)
|
putBoolean(key(type, "enabled"), rule.enabled)
|
||||||
putString(key(type, "app_rule_mode"), rule.appRuleMode.name)
|
putString(key(type, "app_rule_mode"), rule.appRuleMode.name)
|
||||||
putStringSet(key(type, "selected_packages"), rule.selectedPackages)
|
putStringSet(key(type, "selected_packages"), rule.selectedPackages)
|
||||||
@@ -29,12 +43,6 @@ class LoggingRuleStore(context: Context) {
|
|||||||
putBoolean(key(type, "seen_apps_first"), rule.seenAppsFirst)
|
putBoolean(key(type, "seen_apps_first"), rule.seenAppsFirst)
|
||||||
putBoolean(key(type, "ignore_routine_updates"), rule.ignoreRoutineUpdates)
|
putBoolean(key(type, "ignore_routine_updates"), rule.ignoreRoutineUpdates)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun copy(from: LoggingType, targets: Set<LoggingType>) {
|
|
||||||
val source = ruleFor(from)
|
|
||||||
targets.filterNot { it == from }.forEach { save(it, source.copy(selectedPackages = source.selectedPackages.toSet())) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun key(type: LoggingType, suffix: String) = "${type.name.lowercase()}.$suffix"
|
private fun key(type: LoggingType, suffix: String) = "${type.name.lowercase()}.$suffix"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/app_list_refresh" android:layout_width="match_parent" android:layout_height="match_parent"><ScrollView android:layout_width="match_parent" android:layout_height="match_parent">
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/app_list_refresh" android:layout_width="match_parent" android:layout_height="match_parent"><ScrollView android:id="@+id/app_scroll" 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">
|
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" 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" />
|
<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="Logging enabled" />
|
<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="Logging enabled" />
|
||||||
|
|||||||
@@ -1,2 +1,103 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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="Settings" android:textAppearance="?attr/textAppearanceHeadline5"/><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"/><com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/app_lock" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="Lock app with phone unlock"/><TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="Copy event settings"/><Spinner android:id="@+id/copy_from" android:layout_width="match_parent" android:layout_height="wrap_content"/><Button android:id="@+id/copy_to" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Choose targets" android:enabled="false"/><Button android:id="@+id/copy_event_settings" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Copy settings" android:enabled="false"/></LinearLayout></ScrollView>
|
<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="Settings"
|
||||||
|
android:textAppearance="?attr/textAppearanceHeadline5" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="Event logging" />
|
||||||
|
|
||||||
|
<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 event settings" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/copy_from"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/copy_to"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:enabled="false"
|
||||||
|
android:text="Choose targets" />
|
||||||
|
|
||||||
|
<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/export_logs"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Export logs" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/clear_logs"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Clear logs" />
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/app_lock"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="Lock app with phone unlock" />
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|||||||
Reference in New Issue
Block a user