Load event app lists asynchronously
This commit is contained in:
@@ -88,6 +88,7 @@ dependencies {
|
|||||||
implementation(libs.fragment.ktx)
|
implementation(libs.fragment.ktx)
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.drawerlayout)
|
implementation(libs.drawerlayout)
|
||||||
|
implementation(libs.recyclerview)
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
|
|||||||
@@ -1,61 +1,67 @@
|
|||||||
package se.ajpanton.notificationlog
|
package se.ajpanton.notificationlog
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.StyleSpan
|
||||||
|
import android.text.style.UnderlineSpan
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.CheckBox
|
import android.widget.CheckBox
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import se.ajpanton.notificationlog.capture.SeenApps
|
||||||
import se.ajpanton.notificationlog.databinding.FragmentEventSettingsBinding
|
import se.ajpanton.notificationlog.databinding.FragmentEventSettingsBinding
|
||||||
import se.ajpanton.notificationlog.settings.AppRuleMode
|
|
||||||
import se.ajpanton.notificationlog.settings.LoggingRuleStore
|
|
||||||
import se.ajpanton.notificationlog.settings.LoggingType
|
|
||||||
import se.ajpanton.notificationlog.settings.AppListItem
|
import se.ajpanton.notificationlog.settings.AppListItem
|
||||||
import se.ajpanton.notificationlog.settings.AppListOrdering
|
import se.ajpanton.notificationlog.settings.AppListOrdering
|
||||||
|
import se.ajpanton.notificationlog.settings.AppRuleMode
|
||||||
import se.ajpanton.notificationlog.settings.ListedApp
|
import se.ajpanton.notificationlog.settings.ListedApp
|
||||||
import se.ajpanton.notificationlog.capture.SeenApps
|
import se.ajpanton.notificationlog.settings.LoggingRule
|
||||||
|
import se.ajpanton.notificationlog.settings.LoggingRuleStore
|
||||||
|
import se.ajpanton.notificationlog.settings.LoggingType
|
||||||
|
|
||||||
class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) {
|
class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) {
|
||||||
private var binding: FragmentEventSettingsBinding? = null
|
private var binding: FragmentEventSettingsBinding? = null
|
||||||
private lateinit var type: LoggingType
|
private lateinit var type: LoggingType
|
||||||
private lateinit var store: LoggingRuleStore
|
private lateinit var store: LoggingRuleStore
|
||||||
|
private lateinit var appAdapter: AppListAdapter
|
||||||
|
private var appLoadGeneration = 0
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
binding = FragmentEventSettingsBinding.bind(view)
|
binding = FragmentEventSettingsBinding.bind(view)
|
||||||
type = requireArguments().getSerializable(ARG_TYPE, LoggingType::class.java)!!
|
type = requireArguments().getSerializable(ARG_TYPE, LoggingType::class.java)!!
|
||||||
store = LoggingRuleStore(requireContext())
|
store = LoggingRuleStore(requireContext())
|
||||||
|
appAdapter = AppListAdapter(::setPackageSelected)
|
||||||
binding!!.pageTitle.setText(requireArguments().getInt(ARG_TITLE))
|
binding!!.pageTitle.setText(requireArguments().getInt(ARG_TITLE))
|
||||||
|
binding!!.appList.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
binding!!.appList.adapter = appAdapter
|
||||||
|
binding!!.appListRefresh.setOnRefreshListener(::reloadAppList)
|
||||||
|
installBottomRefresh()
|
||||||
bindRule()
|
bindRule()
|
||||||
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() {
|
||||||
|
appLoadGeneration++
|
||||||
|
binding = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
private fun refreshAppList() {
|
private fun installBottomRefresh() {
|
||||||
val currentBinding = binding ?: return
|
val list = binding!!.appList
|
||||||
currentBinding.appListRefresh.isRefreshing = true
|
var touchStartY = 0f
|
||||||
currentBinding.appListRefresh.post {
|
list.setOnTouchListener { _, event ->
|
||||||
if (binding === currentBinding) {
|
when (event.actionMasked) {
|
||||||
reloadAppList()
|
MotionEvent.ACTION_DOWN -> touchStartY = event.y
|
||||||
currentBinding.appListRefresh.isRefreshing = false
|
MotionEvent.ACTION_UP -> if (!list.canScrollVertically(1) && event.y < touchStartY) reloadAppList()
|
||||||
}
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,75 +81,164 @@ class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) {
|
|||||||
binding!!.seenFirstToggle.isEnabled = !rule.onlySeenApps
|
binding!!.seenFirstToggle.isEnabled = !rule.onlySeenApps
|
||||||
binding!!.routineUpdatesToggle.visibility = if (type == LoggingType.EDITS) View.VISIBLE else View.GONE
|
binding!!.routineUpdatesToggle.visibility = if (type == LoggingType.EDITS) View.VISIBLE else View.GONE
|
||||||
binding!!.routineUpdatesToggle.isChecked = rule.ignoreRoutineUpdates
|
binding!!.routineUpdatesToggle.isChecked = rule.ignoreRoutineUpdates
|
||||||
updateRuleLabels()
|
updateRuleLabels(rule)
|
||||||
binding!!.masterToggle.setOnCheckedChangeListener { _, checked -> save { copy(enabled = checked) } }
|
binding!!.masterToggle.setOnCheckedChangeListener { _, checked -> save { copy(enabled = checked) } }
|
||||||
binding!!.appRuleToggle.setOnCheckedChangeListener { _, checked -> save { copy(appRuleMode = if (checked) AppRuleMode.WHITELIST else AppRuleMode.BLACKLIST) } }
|
binding!!.appRuleToggle.setOnCheckedChangeListener { _, checked ->
|
||||||
|
save { copy(appRuleMode = if (checked) AppRuleMode.WHITELIST else AppRuleMode.BLACKLIST) }
|
||||||
|
}
|
||||||
binding!!.onlySeenToggle.setOnCheckedChangeListener { _, checked -> save { copy(onlySeenApps = checked) } }
|
binding!!.onlySeenToggle.setOnCheckedChangeListener { _, checked -> save { copy(onlySeenApps = checked) } }
|
||||||
binding!!.seenFirstToggle.setOnCheckedChangeListener { _, checked -> save { copy(seenAppsFirst = checked) } }
|
binding!!.seenFirstToggle.setOnCheckedChangeListener { _, checked -> save { copy(seenAppsFirst = checked) } }
|
||||||
binding!!.routineUpdatesToggle.setOnCheckedChangeListener { _, checked -> save { copy(ignoreRoutineUpdates = checked) } }
|
binding!!.routineUpdatesToggle.setOnCheckedChangeListener { _, checked -> save { copy(ignoreRoutineUpdates = checked) } }
|
||||||
reloadAppList()
|
reloadAppList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun save(change: se.ajpanton.notificationlog.settings.LoggingRule.() -> se.ajpanton.notificationlog.settings.LoggingRule) {
|
private fun save(change: LoggingRule.() -> LoggingRule) {
|
||||||
store.save(type, store.ruleFor(type).change())
|
store.save(type, store.ruleFor(type).change())
|
||||||
bindRule()
|
bindRule()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateRuleLabels() {
|
private fun updateRuleLabels(rule: LoggingRule) {
|
||||||
val rule = store.ruleFor(type)
|
binding!!.masterToggle.text = "Enable logging"
|
||||||
binding!!.appRuleToggle.text = if (rule.appRuleMode == AppRuleMode.WHITELIST) "Whitelist chosen" else "Blacklist chosen"
|
binding!!.appRuleToggle.text = modeLabel(rule.appRuleMode)
|
||||||
binding!!.onlySeenToggle.text = if (rule.onlySeenApps) "Show only seen apps" else "Show all apps"
|
binding!!.onlySeenToggle.text = "Show only seen apps"
|
||||||
|
binding!!.seenFirstToggle.text = "Show seen apps first"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun modeLabel(mode: AppRuleMode): CharSequence {
|
||||||
|
val text = "Blacklist / Whitelist"
|
||||||
|
val selected = if (mode == AppRuleMode.BLACKLIST) "Blacklist" else "Whitelist"
|
||||||
|
val start = text.indexOf(selected)
|
||||||
|
return SpannableString(text).apply {
|
||||||
|
setSpan(StyleSpan(android.graphics.Typeface.BOLD), start, start + selected.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
setSpan(UnderlineSpan(), start, start + selected.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun reloadAppList() {
|
private fun reloadAppList() {
|
||||||
val container = binding!!.appList
|
val currentBinding = binding ?: return
|
||||||
container.removeAllViews()
|
if (currentBinding.appListRefresh.isRefreshing) {
|
||||||
|
// A fresh swipe replaces the current snapshot; no live updates occur otherwise.
|
||||||
|
}
|
||||||
|
val generation = ++appLoadGeneration
|
||||||
|
currentBinding.appListLoading.visibility = View.VISIBLE
|
||||||
|
currentBinding.appListRefresh.isRefreshing = true
|
||||||
val rule = store.ruleFor(type)
|
val rule = store.ruleFor(type)
|
||||||
val seen = SeenApps.snapshot()
|
val seen = SeenApps.snapshot()
|
||||||
val apps = requireContext().packageManager.getInstalledApplications(0)
|
val context = requireContext().applicationContext
|
||||||
.map { info ->
|
Thread {
|
||||||
|
val apps = context.packageManager.getInstalledApplications(0).map { info ->
|
||||||
ListedApp(
|
ListedApp(
|
||||||
label = requireContext().packageManager.getApplicationLabel(info).toString(),
|
label = context.packageManager.getApplicationLabel(info).toString(),
|
||||||
packageName = info.packageName,
|
packageName = info.packageName,
|
||||||
seen = info.packageName in seen,
|
seen = info.packageName in seen,
|
||||||
selected = info.packageName in rule.selectedPackages,
|
selected = info.packageName in rule.selectedPackages,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AppListOrdering.items(apps, rule.onlySeenApps, rule.seenAppsFirst).forEach { item ->
|
val items = AppListOrdering.items(apps, rule.onlySeenApps, rule.seenAppsFirst)
|
||||||
container.addView(if (item is AppListItem.App) appView(item.value) else separator())
|
activity?.runOnUiThread {
|
||||||
}
|
if (binding === currentBinding && generation == appLoadGeneration) {
|
||||||
}
|
appAdapter.submit(items)
|
||||||
|
currentBinding.appListLoading.visibility = View.GONE
|
||||||
private fun appView(row: ListedApp): View = LinearLayout(requireContext()).apply {
|
currentBinding.appListRefresh.isRefreshing = false
|
||||||
orientation = LinearLayout.HORIZONTAL
|
}
|
||||||
val checkbox = CheckBox(context).apply { isChecked = row.selected }
|
|
||||||
checkbox.setOnCheckedChangeListener { _, checked ->
|
|
||||||
val current = store.ruleFor(type)
|
|
||||||
val packages = current.selectedPackages.toMutableSet().apply {
|
|
||||||
if (checked) add(row.packageName) else remove(row.packageName)
|
|
||||||
}
|
}
|
||||||
store.save(type, current.copy(selectedPackages = packages))
|
}.start()
|
||||||
// Deliberately do not reload: an unchecked unseen row must remain reachable.
|
|
||||||
}
|
|
||||||
addView(checkbox)
|
|
||||||
addView(LinearLayout(context).apply {
|
|
||||||
orientation = LinearLayout.VERTICAL
|
|
||||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
|
||||||
addView(TextView(context).apply { text = row.label; textSize = 16f; setTypeface(typeface, 1) })
|
|
||||||
addView(TextView(context).apply { text = row.packageName; textSize = 12f })
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun separator() = View(requireContext()).apply {
|
private fun setPackageSelected(packageName: String, selected: Boolean) {
|
||||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 1).apply { setMargins(0, 12, 0, 12) }
|
val current = store.ruleFor(type)
|
||||||
setBackgroundColor(0x33000000)
|
val packages = current.selectedPackages.toMutableSet().apply {
|
||||||
|
if (selected) add(packageName) else remove(packageName)
|
||||||
|
}
|
||||||
|
store.save(type, current.copy(selectedPackages = packages))
|
||||||
|
// Do not reload: an unchecked unseen row remains visible until the next requested refresh.
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AppListAdapter(
|
||||||
|
private val onSelectedChanged: (String, Boolean) -> Unit,
|
||||||
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
private var items: List<AppListItem> = emptyList()
|
||||||
|
|
||||||
|
fun submit(newItems: List<AppListItem>) {
|
||||||
|
items = newItems
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int = if (items[position] is AppListItem.App) APP else SEPARATOR
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = if (viewType == APP) {
|
||||||
|
val row = LinearLayout(parent.context).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
setPadding(0, dp(parent, 4), 0, dp(parent, 4))
|
||||||
|
}
|
||||||
|
val checkbox = CheckBox(parent.context)
|
||||||
|
val textColumn = LinearLayout(parent.context).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||||
|
}
|
||||||
|
val label = TextView(parent.context).apply { textSize = 16f; setTypeface(typeface, android.graphics.Typeface.BOLD) }
|
||||||
|
val packageName = TextView(parent.context).apply { textSize = 12f }
|
||||||
|
textColumn.addView(label)
|
||||||
|
textColumn.addView(packageName)
|
||||||
|
row.addView(checkbox)
|
||||||
|
row.addView(textColumn)
|
||||||
|
AppHolder(row, checkbox, label, packageName)
|
||||||
|
} else {
|
||||||
|
val divider = View(parent.context).apply {
|
||||||
|
layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, dp(parent, 1)).apply {
|
||||||
|
setMargins(0, dp(parent, 12), 0, dp(parent, 12))
|
||||||
|
}
|
||||||
|
setBackgroundColor(ContextCompat.getColor(parent.context, android.R.color.darker_gray))
|
||||||
|
}
|
||||||
|
SeparatorHolder(divider)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
val item = items[position]
|
||||||
|
if (holder is AppHolder && item is AppListItem.App) {
|
||||||
|
val app = item.value
|
||||||
|
holder.label.text = app.label
|
||||||
|
holder.packageName.text = app.packageName
|
||||||
|
holder.checkbox.setOnCheckedChangeListener(null)
|
||||||
|
holder.checkbox.isChecked = app.selected
|
||||||
|
holder.checkbox.setOnCheckedChangeListener { _, checked ->
|
||||||
|
onSelectedChanged(app.packageName, checked)
|
||||||
|
items = items.map { current ->
|
||||||
|
if (current is AppListItem.App && current.value.packageName == app.packageName) {
|
||||||
|
AppListItem.App(current.value.copy(selected = checked))
|
||||||
|
} else current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
|
private class AppHolder(
|
||||||
|
view: View,
|
||||||
|
val checkbox: CheckBox,
|
||||||
|
val label: TextView,
|
||||||
|
val packageName: TextView,
|
||||||
|
) : RecyclerView.ViewHolder(view)
|
||||||
|
|
||||||
|
private class SeparatorHolder(view: View) : RecyclerView.ViewHolder(view)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val APP = 0
|
||||||
|
const val SEPARATOR = 1
|
||||||
|
fun dp(parent: ViewGroup, value: Int): Int = (value * parent.resources.displayMetrics.density).toInt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ARG_TITLE = "title"
|
private const val ARG_TITLE = "title"
|
||||||
private const val ARG_TYPE = "type"
|
private const val ARG_TYPE = "type"
|
||||||
|
|
||||||
fun newInstance(@StringRes title: Int, type: LoggingType) = EventSettingsFragment().apply {
|
fun newInstance(@StringRes title: Int, type: LoggingType) = EventSettingsFragment().apply {
|
||||||
arguments = Bundle().apply { putInt(ARG_TITLE, title); putSerializable(ARG_TYPE, type) }
|
arguments = Bundle().apply {
|
||||||
|
putInt(ARG_TITLE, title)
|
||||||
|
putSerializable(ARG_TYPE, type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,18 @@ object AppListOrdering {
|
|||||||
val apps = allApps.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label })
|
val apps = allApps.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label })
|
||||||
val shown = if (onlySeen) apps.filter { it.seen || it.selected } else apps
|
val shown = if (onlySeen) apps.filter { it.seen || it.selected } else apps
|
||||||
val seen = shown.filter { it.seen }
|
val seen = shown.filter { it.seen }
|
||||||
val other = shown.filterNot { it.seen }
|
val selectedUnseen = shown.filter { !it.seen }
|
||||||
val separate = (onlySeen || seenAppsFirst) && seen.isNotEmpty() && other.isNotEmpty()
|
if (!seenAppsFirst || seen.isEmpty()) return shown.map(AppListItem::App)
|
||||||
return buildList {
|
return buildList {
|
||||||
if (separate) {
|
seen.forEach { add(AppListItem.App(it)) }
|
||||||
seen.forEach { add(AppListItem.App(it)) }
|
if (onlySeen) {
|
||||||
add(AppListItem.Separator)
|
if (selectedUnseen.isNotEmpty()) {
|
||||||
other.forEach { add(AppListItem.App(it)) }
|
add(AppListItem.Separator)
|
||||||
|
selectedUnseen.forEach { add(AppListItem.App(it)) }
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
add(AppListItem.Separator)
|
||||||
|
// The full alphabetical list intentionally repeats seen apps after its quick-access group.
|
||||||
shown.forEach { add(AppListItem.App(it)) }
|
shown.forEach { add(AppListItem.App(it)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,83 @@
|
|||||||
<?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:id="@+id/app_scroll" android:layout_width="match_parent" android:layout_height="match_parent">
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="@dimen/page_padding">
|
android:layout_width="match_parent"
|
||||||
<TextView android:id="@+id/page_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAppearance="?attr/textAppearanceHeadline5" />
|
android:layout_height="match_parent"
|
||||||
<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" />
|
android:orientation="vertical"
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/app_rule_toggle" android:layout_width="match_parent" android:layout_height="wrap_content" />
|
android:padding="@dimen/page_padding">
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/only_seen_toggle" android:layout_width="match_parent" android:layout_height="wrap_content" />
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/seen_first_toggle" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Show seen apps at the top" />
|
<TextView
|
||||||
<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" />
|
android:id="@+id/page_title"
|
||||||
<LinearLayout android:id="@+id/app_list" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:orientation="vertical" />
|
android:layout_width="match_parent"
|
||||||
</LinearLayout>
|
android:layout_height="wrap_content"
|
||||||
</ScrollView></androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
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"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/only_seen_toggle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Show only seen apps" />
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/seen_first_toggle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
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"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/app_list_refresh"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/app_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:fastScrollAlwaysVisible="true"
|
||||||
|
android:fastScrollEnabled="true"
|
||||||
|
android:paddingBottom="16dp" />
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/app_list_loading"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="Loading apps…" />
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import org.junit.Test
|
|||||||
class AppListOrderingTest {
|
class AppListOrderingTest {
|
||||||
private fun app(label: String, seen: Boolean = false, selected: Boolean = false) = ListedApp(label, "pkg.$label", seen, selected)
|
private fun app(label: String, seen: Boolean = false, selected: Boolean = false) = ListedApp(label, "pkg.$label", seen, selected)
|
||||||
|
|
||||||
@Test fun `seen apps are first with a separator when requested`() {
|
@Test fun `seen apps are repeated before the full list when requested`() {
|
||||||
val items = AppListOrdering.items(listOf(app("Zulu"), app("Beta", seen = true), app("Alpha", seen = true)), false, true)
|
val items = AppListOrdering.items(listOf(app("Zulu"), app("Beta", seen = true), app("Alpha", seen = true)), false, true)
|
||||||
assertEquals(listOf("Alpha", "Beta", "|", "Zulu"), items.map { if (it is AppListItem.App) it.value.label else "|" })
|
assertEquals(listOf("Alpha", "Beta", "|", "Alpha", "Beta", "Zulu"), items.map { if (it is AppListItem.App) it.value.label else "|" })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun `selected unseen app remains after seen apps in only-seen mode`() {
|
@Test fun `selected unseen app remains after seen apps in only-seen mode`() {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ coreKtx = "1.13.1"
|
|||||||
fragmentKtx = "1.8.5"
|
fragmentKtx = "1.8.5"
|
||||||
material = "1.10.0"
|
material = "1.10.0"
|
||||||
drawerLayout = "1.2.0"
|
drawerLayout = "1.2.0"
|
||||||
|
recyclerView = "1.3.2"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
@@ -14,6 +15,7 @@ core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx"
|
|||||||
fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
|
fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
|
||||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
drawerlayout = { group = "androidx.drawerlayout", name = "drawerlayout", version.ref = "drawerLayout" }
|
drawerlayout = { group = "androidx.drawerlayout", name = "drawerlayout", version.ref = "drawerLayout" }
|
||||||
|
recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerView" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|||||||
Reference in New Issue
Block a user