Redesign notification visibility controls

This commit is contained in:
ajp_anton
2026-08-31 01:18:40 +00:00
parent a7c9a2a22f
commit dc11e605d1
8 changed files with 318 additions and 123 deletions
@@ -1,19 +1,23 @@
package se.ajpanton.notificationsmaster package se.ajpanton.notificationsmaster
import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText import android.widget.EditText
import android.widget.LinearLayout import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.switchmaterial.SwitchMaterial import com.google.android.material.switchmaterial.SwitchMaterial
import se.ajpanton.notificationsmaster.databinding.FragmentAppVisibilityBinding import se.ajpanton.notificationsmaster.databinding.FragmentAppVisibilityBinding
import se.ajpanton.notificationsmaster.databinding.ItemVisibilityExceptionBinding
import se.ajpanton.notificationsmaster.visibility.AppVisibilityPolicy import se.ajpanton.notificationsmaster.visibility.AppVisibilityPolicy
import se.ajpanton.notificationsmaster.visibility.NotificationSurface import se.ajpanton.notificationsmaster.visibility.NotificationSurface
import se.ajpanton.notificationsmaster.visibility.VisibilityExceptionRule import se.ajpanton.notificationsmaster.visibility.VisibilityExceptionRule
@@ -24,7 +28,8 @@ class AppVisibilityFragment : Fragment(R.layout.fragment_app_visibility) {
private var binding: FragmentAppVisibilityBinding? = null private var binding: FragmentAppVisibilityBinding? = null
private lateinit var store: VisibilityPolicyStore private lateinit var store: VisibilityPolicyStore
private lateinit var packageName: String private lateinit var packageName: String
private val adapter = ExceptionAdapter(::editException, ::deleteException) private lateinit var adapter: ExceptionAdapter
private lateinit var touchHelper: ItemTouchHelper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = FragmentAppVisibilityBinding.bind(view) binding = FragmentAppVisibilityBinding.bind(view)
@@ -32,17 +37,29 @@ class AppVisibilityFragment : Fragment(R.layout.fragment_app_visibility) {
packageName = requireArguments().getString(ARG_PACKAGE)!! packageName = requireArguments().getString(ARG_PACKAGE)!!
binding!!.appName.text = InstalledApps.name(requireContext(), packageName) binding!!.appName.text = InstalledApps.name(requireContext(), packageName)
binding!!.packageName.text = packageName binding!!.packageName.text = packageName
binding!!.returnToList.setOnClickListener(::returnToList)
val icon = requireContext().packageManager.getApplicationIcon(packageName)
adapter = ExceptionAdapter(icon, ::editException, ::deleteException, ::saveExceptions) {
touchHelper.startDrag(it)
}
binding!!.exceptions.layoutManager = LinearLayoutManager(requireContext()) binding!!.exceptions.layoutManager = LinearLayoutManager(requireContext())
binding!!.exceptions.adapter = adapter binding!!.exceptions.adapter = adapter
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) { touchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
override fun onMove(list: RecyclerView, from: RecyclerView.ViewHolder, to: RecyclerView.ViewHolder): Boolean { override fun isLongPressDragEnabled() = false
override fun onMove(
list: RecyclerView,
from: RecyclerView.ViewHolder,
to: RecyclerView.ViewHolder,
): Boolean {
adapter.move(from.bindingAdapterPosition, to.bindingAdapterPosition) adapter.move(from.bindingAdapterPosition, to.bindingAdapterPosition)
saveApp { copy(exceptions = adapter.rules()) } saveExceptions(adapter.rules())
return true return true
} }
override fun onSwiped(holder: RecyclerView.ViewHolder, direction: Int) = Unit override fun onSwiped(holder: RecyclerView.ViewHolder, direction: Int) = Unit
}).attachToRecyclerView(binding!!.exceptions) }).also { it.attachToRecyclerView(binding!!.exceptions) }
binding!!.addException.setOnClickListener { showExceptionDialog(null) } binding!!.addException.setOnClickListener { showExceptionDialog(null) }
bind() bind()
} }
@@ -57,21 +74,27 @@ class AppVisibilityFragment : Fragment(R.layout.fragment_app_visibility) {
super.onDestroyView() super.onDestroyView()
} }
private fun returnToList(@Suppress("UNUSED_PARAMETER") view: View) {
if (!parentFragmentManager.popBackStackImmediate()) {
parentFragmentManager.commit { replace(R.id.content_frame, NotificationVisibilityFragment()) }
}
}
private fun bind() { private fun bind() {
val policy = appPolicy() val policy = appPolicy()
bindSurface(binding!!.hideUnlocked, NotificationSurface.UNLOCKED_STATUSBAR, policy) bindSurface(binding!!.showAod, NotificationSurface.AOD, policy)
bindSurface(binding!!.hideLockscreen, NotificationSurface.LOCKSCREEN_COLLAPSED, policy) bindSurface(binding!!.showLockscreen, NotificationSurface.LOCKSCREEN_COLLAPSED, policy)
bindSurface(binding!!.hideAod, NotificationSurface.AOD, policy) bindSurface(binding!!.showUnlocked, NotificationSurface.UNLOCKED_STATUSBAR, policy)
adapter.submit(policy.exceptions) adapter.submit(policy.exceptions)
} }
private fun bindSurface(toggle: SwitchMaterial, surface: NotificationSurface, policy: AppVisibilityPolicy) { private fun bindSurface(toggle: SwitchMaterial, surface: NotificationSurface, policy: AppVisibilityPolicy) {
toggle.setOnCheckedChangeListener(null) toggle.setOnCheckedChangeListener(null)
toggle.isChecked = surface in policy.blockedSurfaces toggle.isChecked = surface !in policy.blockedSurfaces
toggle.setOnCheckedChangeListener { _, hidden -> toggle.setOnCheckedChangeListener { _, visible ->
saveApp { saveApp {
copy(blockedSurfaces = blockedSurfaces.toMutableSet().apply { copy(blockedSurfaces = blockedSurfaces.toMutableSet().apply {
if (hidden) add(surface) else remove(surface) if (visible) remove(surface) else add(surface)
}) })
} }
} }
@@ -90,59 +113,46 @@ class AppVisibilityFragment : Fragment(R.layout.fragment_app_visibility) {
} }
} }
private fun saveExceptions(rules: List<VisibilityExceptionRule>) {
saveApp { copy(exceptions = rules) }
}
private fun editException(position: Int) = showExceptionDialog(position) private fun editException(position: Int) = showExceptionDialog(position)
private fun deleteException(position: Int) { private fun deleteException(position: Int) {
saveApp { copy(exceptions = exceptions.toMutableList().apply { removeAt(position) }) } adapter.submit(adapter.rules().toMutableList().apply { removeAt(position) })
bind() saveExceptions(adapter.rules())
} }
private fun showExceptionDialog(position: Int?) { private fun showExceptionDialog(position: Int?) {
val existing = position?.let { appPolicy().exceptions[it] } val existing = position?.let { adapter.rules().getOrNull(it) }
val content = LinearLayout(requireContext()).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(24), 0, dp(24), 0)
}
val pattern = EditText(requireContext()).apply { val pattern = EditText(requireContext()).apply {
hint = "Regular expression" hint = "Regular expression"
setText(existing?.pattern) setText(existing?.pattern)
maxLines = 4 maxLines = 4
} setPadding(dp(24), 0, dp(24), 0)
content.addView(pattern)
val toggles = listOf(
NotificationSurface.UNLOCKED_STATUSBAR to "Hide in unlocked status bar",
NotificationSurface.LOCKSCREEN_COLLAPSED to "Hide on collapsed lockscreen",
NotificationSurface.AOD to "Hide on always-on display",
).associate { (surface, label) ->
surface to SwitchMaterial(requireContext()).apply {
text = label
isChecked = surface in existing?.blockedSurfaces.orEmpty()
content.addView(this)
}
} }
val dialog = MaterialAlertDialogBuilder(requireContext()) val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(if (existing == null) "Add text exception" else "Edit text exception") .setTitle(if (existing == null) "Add text exception" else "Edit text exception")
.setView(content) .setView(pattern)
.setNegativeButton("Cancel", null) .setNegativeButton("Cancel", null)
.setPositiveButton("Save", null) .setPositiveButton("Save", null)
.create() .create()
dialog.setOnShowListener { dialog.setOnShowListener {
dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener { dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val rule = runCatching { val rule = VisibilityExceptionRule(
VisibilityExceptionRule(
pattern.text.toString(), pattern.text.toString(),
toggles.filterValues { it.isChecked }.keys, existing?.blockedSurfaces ?: appPolicy().blockedSurfaces,
).also { require(it.hasValidPattern()) } )
}.getOrElse { if (!rule.hasValidPattern()) {
pattern.error = "Enter a valid, non-empty regular expression" pattern.error = "Enter a valid, non-empty regular expression"
return@setOnClickListener return@setOnClickListener
} }
saveApp { val rules = adapter.rules().toMutableList().apply {
copy(exceptions = exceptions.toMutableList().apply {
if (position == null) add(rule) else set(position, rule) if (position == null) add(rule) else set(position, rule)
})
} }
bind() adapter.submit(rules)
saveExceptions(rules)
dialog.dismiss() dialog.dismiss()
} }
} }
@@ -152,8 +162,11 @@ class AppVisibilityFragment : Fragment(R.layout.fragment_app_visibility) {
private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt() private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt()
private class ExceptionAdapter( private class ExceptionAdapter(
private val appIcon: Drawable,
private val edit: (Int) -> Unit, private val edit: (Int) -> Unit,
private val delete: (Int) -> Unit, private val delete: (Int) -> Unit,
private val changed: (List<VisibilityExceptionRule>) -> Unit,
private val startDrag: (ExceptionHolder) -> Unit,
) : RecyclerView.Adapter<ExceptionHolder>() { ) : RecyclerView.Adapter<ExceptionHolder>() {
private val items = mutableListOf<VisibilityExceptionRule>() private val items = mutableListOf<VisibilityExceptionRule>()
@@ -170,74 +183,72 @@ class AppVisibilityFragment : Fragment(R.layout.fragment_app_visibility) {
fun rules() = items.toList() fun rules() = items.toList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExceptionHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ExceptionHolder(
val row = LinearLayout(parent.context).apply { ItemVisibilityExceptionBinding.inflate(LayoutInflater.from(parent.context), parent, false),
orientation = LinearLayout.HORIZONTAL )
gravity = android.view.Gravity.CENTER_VERTICAL
setPadding(0, dp(parent, 6), 0, dp(parent, 6))
isClickable = true
isFocusable = true
background = context.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackground))
.let { values -> values.getDrawable(0).also { values.recycle() } }
}
val handle = TextView(parent.context).apply {
text = ""
textSize = 22f
gravity = android.view.Gravity.CENTER
contentDescription = "Drag to reorder"
layoutParams = LinearLayout.LayoutParams(dp(parent, 40), dp(parent, 48))
}
val textColumn = LinearLayout(parent.context).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}
val pattern = TextView(parent.context).apply { textSize = 15f }
val summary = TextView(parent.context).apply { textSize = 12f }
textColumn.addView(pattern)
textColumn.addView(summary)
val remove = Button(parent.context).apply {
text = "Delete"
isAllCaps = false
}
row.addView(handle)
row.addView(textColumn)
row.addView(remove)
return ExceptionHolder(row, pattern, summary, remove)
}
override fun onBindViewHolder(holder: ExceptionHolder, position: Int) { override fun onBindViewHolder(holder: ExceptionHolder, position: Int) {
val binding = holder.binding
val rule = items[position] val rule = items[position]
holder.pattern.text = rule.pattern binding.pattern.text = rule.pattern
holder.summary.text = surfaceSummary(rule.blockedSurfaces) bindSurface(binding.aodIcon, binding.aodCross, rule, NotificationSurface.AOD)
holder.itemView.setOnClickListener { bindSurface(
holder.bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION }?.let(edit) binding.lockscreenIcon,
binding.lockscreenCross,
rule,
NotificationSurface.LOCKSCREEN_COLLAPSED,
)
bindSurface(
binding.unlockedIcon,
binding.unlockedCross,
rule,
NotificationSurface.UNLOCKED_STATUSBAR,
)
binding.aodCell.setOnClickListener { toggle(holder, NotificationSurface.AOD) }
binding.lockscreenCell.setOnClickListener {
toggle(holder, NotificationSurface.LOCKSCREEN_COLLAPSED)
} }
holder.remove.setOnClickListener { binding.unlockedCell.setOnClickListener {
holder.bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION }?.let(delete) toggle(holder, NotificationSurface.UNLOCKED_STATUSBAR)
}
binding.edit.setOnClickListener { holder.positionOrNull()?.let(edit) }
binding.delete.setOnClickListener { holder.positionOrNull()?.let(delete) }
binding.dragHandle.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) startDrag(holder)
false
} }
} }
override fun getItemCount() = items.size override fun getItemCount() = items.size
private fun surfaceSummary(surfaces: Set<NotificationSurface>): String { private fun toggle(holder: ExceptionHolder, surface: NotificationSurface) {
val names = listOf( val position = holder.positionOrNull() ?: return
NotificationSurface.UNLOCKED_STATUSBAR to "status bar", val current = items[position]
NotificationSurface.LOCKSCREEN_COLLAPSED to "lockscreen", items[position] = current.copy(blockedSurfaces = current.blockedSurfaces.toMutableSet().apply {
NotificationSurface.AOD to "AOD", if (surface in current.blockedSurfaces) remove(surface) else add(surface)
).filter { it.first in surfaces }.joinToString { it.second } })
return if (names.isEmpty()) "Show on all surfaces" else "Hide: $names" notifyItemChanged(position)
changed(rules())
} }
private fun dp(parent: ViewGroup, value: Int) = private fun bindSurface(
(value * parent.resources.displayMetrics.density).toInt() icon: ImageView,
cross: TextView,
rule: VisibilityExceptionRule,
surface: NotificationSurface,
) {
icon.setImageDrawable(appIcon.constantState?.newDrawable()?.mutate() ?: appIcon)
val hidden = surface in rule.blockedSurfaces
icon.alpha = if (hidden) 0.25f else 1f
cross.visibility = if (hidden) View.VISIBLE else View.GONE
} }
private class ExceptionHolder( private fun ExceptionHolder.positionOrNull() =
view: View, bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION }
val pattern: TextView, }
val summary: TextView,
val remove: Button, private class ExceptionHolder(val binding: ItemVisibilityExceptionBinding) :
) : RecyclerView.ViewHolder(view) RecyclerView.ViewHolder(binding.root)
companion object { companion object {
private const val ARG_PACKAGE = "package" private const val ARG_PACKAGE = "package"
@@ -78,7 +78,15 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
}) })
currentItemId = savedInstanceState?.getInt(STATE_CURRENT_ITEM_ID) ?: R.id.nav_view_logs currentItemId = savedInstanceState?.getInt(STATE_CURRENT_ITEM_ID) ?: R.id.nav_view_logs
if (savedInstanceState == null) {
navigateTo(currentItemId) navigateTo(currentItemId)
} else {
val page = Page.fromMenuItem(currentItemId)
refreshLogProtection(requestUnlock = hasWindowFocus())
syncDrawerSelection(currentItemId)
title = getString(page.titleRes)
binding.root.post(::applyPageTitleVisibility)
}
applyNavigationDrawerInsets() applyNavigationDrawerInsets()
syncDrawerToggle() syncDrawerToggle()
} }
@@ -156,6 +164,10 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
refreshLogProtection(requestUnlock = hasWindowFocus()) refreshLogProtection(requestUnlock = hasWindowFocus())
syncDrawerSelection(itemId) syncDrawerSelection(itemId)
val page = Page.fromMenuItem(itemId) val page = Page.fromMenuItem(itemId)
supportFragmentManager.popBackStackImmediate(
null,
androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE,
)
supportFragmentManager.commit { supportFragmentManager.commit {
replace( replace(
R.id.content_frame, R.id.content_frame,
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,6L18,18M18,6L6,18"
android:strokeColor="@color/visibility_hidden_cross"
android:strokeLineCap="round"
android:strokeWidth="2.5" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M7,4h3v3H7zM14,4h3v3h-3zM7,10.5h3v3H7zM14,10.5h3v3h-3zM7,17h3v3H7zM14,17h3v3h-3z" />
</vector>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34a0.996,0.996 0,0 0,-1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>
@@ -5,10 +5,18 @@
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/page_padding"> android:padding="@dimen/page_padding">
<com.google.android.material.button.MaterialButton
android:id="@+id/return_to_list"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Return to list" />
<TextView <TextView
android:id="@+id/app_name" android:id="@+id/app_name"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="?attr/textAppearanceHeadline6" android:textAppearance="?attr/textAppearanceHeadline6"
android:textStyle="bold" /> android:textStyle="bold" />
@@ -22,52 +30,45 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="20dp" android:layout_marginTop="20dp"
android:text="Hide by default" android:text="Default visibility"
android:textStyle="bold" /> android:textAppearance="?attr/textAppearanceHeadline6" />
<com.google.android.material.switchmaterial.SwitchMaterial <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/hide_aod" android:id="@+id/show_aod"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Always-on display" /> android:text="Always-on display" />
<com.google.android.material.switchmaterial.SwitchMaterial <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/hide_lockscreen" android:id="@+id/show_lockscreen"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Collapsed lockscreen" /> android:text="Collapsed lockscreen" />
<com.google.android.material.switchmaterial.SwitchMaterial <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/hide_unlocked" android:id="@+id/show_unlocked"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Unlocked status bar" /> android:text="Unlocked status bar" />
<LinearLayout <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="20dp" android:layout_marginTop="20dp"
android:gravity="center_vertical" android:text="Exceptions based on text contents"
android:orientation="horizontal"> android:textAppearance="?attr/textAppearanceHeadline6" />
<TextView <com.google.android.material.button.MaterialButton
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Ordered text exceptions"
android:textStyle="bold" />
<Button
android:id="@+id/add_exception" android:id="@+id/add_exception"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Add" /> android:layout_marginTop="4dp"
</LinearLayout> android:text="Add exception" />
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="The first matching expression replaces the defaults above. Drag to reorder." android:text="The first matching expression stops evaluating and replaces default visibility. Drag to reorder."
android:textAppearance="?attr/textAppearanceBody2" /> android:textAppearance="?attr/textAppearanceBody2" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
@@ -22,7 +22,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:text="Tap an app icon to hide or show it on AOD, the lockscreen, or the unlocked status bar. Tap the app name for text exceptions." android:text="Icons represent AOD, Lockscreen and Unlocked, respectively. Tap to toggle visibility.&#10;Tap the app for more advanced options."
android:textAppearance="?attr/textAppearanceCaption" /> android:textAppearance="?attr/textAppearanceCaption" />
<EditText <EditText
@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="64dp"
android:orientation="horizontal"
android:paddingTop="6dp"
android:paddingBottom="6dp">
<ImageView
android:id="@+id/drag_handle"
android:layout_width="40dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Drag to reorder"
android:padding="8dp"
android:src="@drawable/ic_drag_dots"
app:tint="?android:attr/textColorSecondary" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/pattern"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="?attr/textAppearanceBody1" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="40dp"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/aod_cell"
android:layout_width="34dp"
android:layout_height="40dp"
android:foreground="?attr/selectableItemBackgroundBorderless">
<ImageView
android:id="@+id/aod_icon"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_gravity="center"
android:contentDescription="AOD visibility"
android:scaleType="fitCenter" />
<TextView
android:id="@+id/aod_cross"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="✕"
android:textColor="@color/visibility_hidden_cross"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="gone" />
</FrameLayout>
<FrameLayout
android:id="@+id/lockscreen_cell"
android:layout_width="34dp"
android:layout_height="40dp"
android:foreground="?attr/selectableItemBackgroundBorderless">
<ImageView
android:id="@+id/lockscreen_icon"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_gravity="center"
android:contentDescription="Lockscreen visibility"
android:scaleType="fitCenter" />
<TextView
android:id="@+id/lockscreen_cross"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="✕"
android:textColor="@color/visibility_hidden_cross"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="gone" />
</FrameLayout>
<FrameLayout
android:id="@+id/unlocked_cell"
android:layout_width="34dp"
android:layout_height="40dp"
android:foreground="?attr/selectableItemBackgroundBorderless">
<ImageView
android:id="@+id/unlocked_icon"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_gravity="center"
android:contentDescription="Unlocked status-bar visibility"
android:scaleType="fitCenter" />
<TextView
android:id="@+id/unlocked_cross"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="✕"
android:textColor="@color/visibility_hidden_cross"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
</LinearLayout>
<ImageButton
android:id="@+id/edit"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Edit expression"
android:padding="12dp"
android:src="@drawable/ic_edit"
app:tint="?android:attr/textColorPrimary" />
<ImageButton
android:id="@+id/delete"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Delete expression"
android:padding="12dp"
android:src="@drawable/ic_delete_cross" />
</LinearLayout>