Render OneUI lockscreen Cards overflow grid
This commit is contained in:
@@ -0,0 +1,307 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.module
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import se.ajpanton.notificationsmaster.visibility.CardsGridLayout
|
||||||
|
import se.ajpanton.notificationsmaster.visibility.CardsGridSettings
|
||||||
|
import java.util.LinkedHashMap
|
||||||
|
import java.util.WeakHashMap
|
||||||
|
|
||||||
|
/** Renders a OneUI Cards-mode icon grid from notification icon snapshots. */
|
||||||
|
internal class OneUiCardsGridRenderer {
|
||||||
|
private val states = WeakHashMap<ViewGroup, State>()
|
||||||
|
|
||||||
|
fun updateSources(container: ViewGroup, sources: List<Source>) {
|
||||||
|
val unique = LinkedHashMap<String, Source>()
|
||||||
|
sources.forEach { unique.putIfAbsent(it.key, it) }
|
||||||
|
state(container).sources = unique.values.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh(container: ViewGroup, active: Boolean, visibleCardKeys: Set<String>, config: Config) {
|
||||||
|
val state = state(container)
|
||||||
|
state.active = active
|
||||||
|
state.visibleCardKeys = visibleCardKeys
|
||||||
|
state.config = config
|
||||||
|
container.post { render(container, state, state.active) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshAll(
|
||||||
|
active: (ViewGroup) -> Boolean,
|
||||||
|
visibleCardKeys: (ViewGroup) -> Set<String>,
|
||||||
|
config: Config,
|
||||||
|
) {
|
||||||
|
states.keys.toList().forEach { container ->
|
||||||
|
refresh(container, active(container), visibleCardKeys(container), config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun render(container: ViewGroup, state: State, active: Boolean) {
|
||||||
|
if (!active || !container.isAttachedToWindow) {
|
||||||
|
failOpen(state)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val root = container.rootView as? ViewGroup ?: return failOpen(state)
|
||||||
|
val remaining = state.sources.filterNot { it.key in state.visibleCardKeys }
|
||||||
|
if (remaining.isEmpty()) {
|
||||||
|
failOpen(state)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val host = attachHost(root, container, state) ?: return failOpen(state)
|
||||||
|
val iconSize = iconSize(root, container, state.config.settings)
|
||||||
|
val settings = state.config.settings
|
||||||
|
val horizontalPadding = dp(root, 8)
|
||||||
|
val widthCapacity = if (iconSize > 0) {
|
||||||
|
(root.width - horizontalPadding * 2).coerceAtLeast(0) / iconSize
|
||||||
|
} else 0
|
||||||
|
val rows = CardsGridLayout.arrange(remaining.size, settings, widthCapacity)
|
||||||
|
val location = relativeLocation(container, root) ?: return failOpen(state)
|
||||||
|
if (root.width <= 0 || rows.visibleSlots == 0) return failOpen(state)
|
||||||
|
val iconCount = rows.visibleSlots - if (rows.showsOverflow) 1 else 0
|
||||||
|
val icons = remaining.take(iconCount).map { notificationIcon(root, it, iconSize) }
|
||||||
|
if (icons.any { it == null }) {
|
||||||
|
icons.filterNotNull().forEach(::recycleIcon)
|
||||||
|
return failOpen(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearHost(host)
|
||||||
|
host.visibility = View.VISIBLE
|
||||||
|
val rowGap = dp(root, 8)
|
||||||
|
val nativeSurface = if (state.config.hideShelf) findNativeSurface(container) else container
|
||||||
|
if (nativeSurface === root) return failOpen(state)
|
||||||
|
var sourceIndex = 0
|
||||||
|
var slotIndex = 0
|
||||||
|
rows.rowSizes.forEachIndexed { row, count ->
|
||||||
|
val rowWidth = count * iconSize
|
||||||
|
val left = (location.first + container.width / 2 - rowWidth / 2).let {
|
||||||
|
if (settings.limitToScreenWidth) {
|
||||||
|
it.coerceIn(horizontalPadding, (root.width - rowWidth - horizontalPadding).coerceAtLeast(horizontalPadding))
|
||||||
|
} else it
|
||||||
|
}
|
||||||
|
val top = location.second + row * (iconSize + rowGap)
|
||||||
|
if (state.config.showPills) {
|
||||||
|
host.addView(pill(root, nativeSurface, left, top, rowWidth, iconSize))
|
||||||
|
}
|
||||||
|
repeat(count) { column ->
|
||||||
|
val overflow = rows.showsOverflow && slotIndex == rows.visibleSlots - 1
|
||||||
|
val icon = if (overflow) overflowDot(root, iconSize) else icons[sourceIndex++]!!
|
||||||
|
host.addView(icon, frame(iconSize, iconSize, left + column * iconSize, top))
|
||||||
|
slotIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hideNative(state, nativeSurface)
|
||||||
|
val diagnosticSignature = listOf(remaining.size, state.visibleCardKeys.size, rows.visibleSlots,
|
||||||
|
settings.maxRows, settings.maxIconsPerRow, iconSize).hashCode()
|
||||||
|
if (diagnosticSignature != state.lastDiagnosticSignature) {
|
||||||
|
state.lastDiagnosticSignature = diagnosticSignature
|
||||||
|
Log.i(TAG, "Rendered OneUI Cards grid: cards=${state.visibleCardKeys.size} " +
|
||||||
|
"remaining=${remaining.size} slots=${rows.visibleSlots} overflow=${rows.showsOverflow}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attachHost(root: ViewGroup, container: ViewGroup, state: State): FrameLayout? {
|
||||||
|
state.host?.takeIf { it.parent === root }?.let { return it }
|
||||||
|
state.layoutListener?.let { listener -> state.root?.removeOnLayoutChangeListener(listener) }
|
||||||
|
state.host?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
||||||
|
return runCatching {
|
||||||
|
FrameLayout(root.context).apply {
|
||||||
|
tag = HOST_TAG
|
||||||
|
clipChildren = false
|
||||||
|
clipToPadding = false
|
||||||
|
isClickable = false
|
||||||
|
isFocusable = false
|
||||||
|
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||||
|
root.addView(this, ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
))
|
||||||
|
state.host = this
|
||||||
|
state.root = root
|
||||||
|
val listener = View.OnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
|
||||||
|
if (right - left != oldRight - oldLeft || bottom - top != oldBottom - oldTop) {
|
||||||
|
refresh(container, state.active, state.visibleCardKeys, state.config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.layoutListener = listener
|
||||||
|
root.addOnLayoutChangeListener(listener)
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notificationIcon(root: View, source: Source, size: Int): ImageView? {
|
||||||
|
val drawable = findDrawable(source.view)
|
||||||
|
?: runCatching {
|
||||||
|
val context = source.packageName?.let { root.context.createPackageContext(it, 0) }
|
||||||
|
?: root.context
|
||||||
|
source.smallIcon?.loadDrawable(context)
|
||||||
|
}.getOrNull()
|
||||||
|
?: return null
|
||||||
|
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||||
|
val bounds = Rect(drawable.bounds)
|
||||||
|
try {
|
||||||
|
drawable.bounds = aspectFit(drawable, bounds, size)
|
||||||
|
drawable.draw(Canvas(bitmap))
|
||||||
|
} finally {
|
||||||
|
drawable.bounds = bounds
|
||||||
|
}
|
||||||
|
return ImageView(root.context).apply {
|
||||||
|
setImageBitmap(bitmap)
|
||||||
|
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun overflowDot(root: View, size: Int) = View(root.context).apply {
|
||||||
|
background = object : GradientDrawable() {
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.WHITE }
|
||||||
|
val radius = size * 0.125f
|
||||||
|
canvas.drawCircle(size / 2f, size / 2f, radius, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pill(root: View, native: View, left: Int, top: Int, width: Int, height: Int): View {
|
||||||
|
val horizontal = dp(root, 8)
|
||||||
|
val vertical = dp(root, 3)
|
||||||
|
return View(root.context).apply {
|
||||||
|
background = native.background?.constantState?.newDrawable(root.resources)?.mutate()
|
||||||
|
?: GradientDrawable().apply {
|
||||||
|
shape = GradientDrawable.RECTANGLE
|
||||||
|
cornerRadius = (height + vertical * 2) / 2f
|
||||||
|
setColor(DEFAULT_PILL_COLOR)
|
||||||
|
}
|
||||||
|
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||||
|
layoutParams = frame(
|
||||||
|
width + horizontal * 2,
|
||||||
|
height + vertical * 2,
|
||||||
|
left - horizontal,
|
||||||
|
top - vertical,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun iconSize(root: View, source: View, settings: CardsGridSettings): Int {
|
||||||
|
val id = root.resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||||
|
val statusBarHeight = if (id != 0) root.resources.getDimensionPixelSize(id) else 0
|
||||||
|
val fallback = minOf(source.width, source.height).coerceAtLeast(1)
|
||||||
|
return if (statusBarHeight > 0) {
|
||||||
|
(statusBarHeight * settings.iconHeightPercent / 100f).toInt().coerceAtLeast(1)
|
||||||
|
} else fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun relativeLocation(view: View, root: View): Pair<Int, Int>? = runCatching {
|
||||||
|
val viewLocation = IntArray(2)
|
||||||
|
val rootLocation = IntArray(2)
|
||||||
|
view.getLocationOnScreen(viewLocation)
|
||||||
|
root.getLocationOnScreen(rootLocation)
|
||||||
|
viewLocation[0] - rootLocation[0] to viewLocation[1] - rootLocation[1]
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
private fun findNativeSurface(container: ViewGroup): View {
|
||||||
|
var current = container.parent as? View
|
||||||
|
while (current != null && current !== container.rootView) {
|
||||||
|
if (current.javaClass.name.contains("NotificationShelf")) return current
|
||||||
|
current = current.parent as? View
|
||||||
|
}
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearHost(host: ViewGroup) {
|
||||||
|
repeat(host.childCount) { index -> recycleIcon(host.getChildAt(index)) }
|
||||||
|
host.removeAllViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun recycleIcon(view: View) {
|
||||||
|
((view as? ImageView)?.drawable as? BitmapDrawable)?.bitmap
|
||||||
|
?.takeUnless(Bitmap::isRecycled)?.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun failOpen(state: State) {
|
||||||
|
restoreNative(state)
|
||||||
|
state.host?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideNative(state: State, native: View) {
|
||||||
|
if (state.nativeSurface !== native) {
|
||||||
|
restoreNative(state)
|
||||||
|
state.nativeSurface = native
|
||||||
|
state.nativeAlpha = native.alpha
|
||||||
|
}
|
||||||
|
native.alpha = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreNative(state: State) {
|
||||||
|
state.nativeSurface?.alpha = state.nativeAlpha
|
||||||
|
state.nativeSurface = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findDrawable(view: View?): Drawable? = when (view) {
|
||||||
|
is ImageView -> view.drawable
|
||||||
|
is ViewGroup -> (0 until view.childCount).firstNotNullOfOrNull { findDrawable(view.getChildAt(it)) }
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aspectFit(drawable: Drawable, fallback: Rect, size: Int): Rect {
|
||||||
|
val width = drawable.intrinsicWidth.takeIf { it > 0 } ?: fallback.width().coerceAtLeast(1)
|
||||||
|
val height = drawable.intrinsicHeight.takeIf { it > 0 } ?: fallback.height().coerceAtLeast(1)
|
||||||
|
val scale = minOf(size.toFloat() / width, size.toFloat() / height)
|
||||||
|
val targetWidth = (width * scale).toInt().coerceAtLeast(1)
|
||||||
|
val targetHeight = (height * scale).toInt().coerceAtLeast(1)
|
||||||
|
val left = (size - targetWidth) / 2
|
||||||
|
val top = (size - targetHeight) / 2
|
||||||
|
return Rect(left, top, left + targetWidth, top + targetHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun frame(width: Int, height: Int, left: Int, top: Int) =
|
||||||
|
FrameLayout.LayoutParams(width, height).apply {
|
||||||
|
leftMargin = left
|
||||||
|
topMargin = top
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dp(view: View, value: Int) = (value * view.resources.displayMetrics.density).toInt()
|
||||||
|
|
||||||
|
private fun state(container: ViewGroup) = states.getOrPut(container, ::State)
|
||||||
|
|
||||||
|
data class Source(
|
||||||
|
val key: String,
|
||||||
|
val view: View?,
|
||||||
|
val smallIcon: android.graphics.drawable.Icon?,
|
||||||
|
val packageName: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Config(
|
||||||
|
val settings: CardsGridSettings,
|
||||||
|
val showPills: Boolean,
|
||||||
|
val hideShelf: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
private class State {
|
||||||
|
var sources = emptyList<Source>()
|
||||||
|
var visibleCardKeys = emptySet<String>()
|
||||||
|
var host: FrameLayout? = null
|
||||||
|
var root: ViewGroup? = null
|
||||||
|
var layoutListener: View.OnLayoutChangeListener? = null
|
||||||
|
var active = false
|
||||||
|
var nativeSurface: View? = null
|
||||||
|
var nativeAlpha = 1f
|
||||||
|
var lastDiagnosticSignature = 0
|
||||||
|
var config = Config(CardsGridSettings.LOCKSCREEN_DEFAULT, showPills = true, hideShelf = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val HOST_TAG = "notifications-master:cards-grid"
|
||||||
|
const val DEFAULT_PILL_COLOR = 0xAA202124.toInt()
|
||||||
|
const val TAG = "NotificationsMaster"
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -11,6 +11,7 @@ import java.lang.reflect.Field
|
|||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.WeakHashMap
|
import java.util.WeakHashMap
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
/** Hides blocked OneUI notification rows only on the collapsed cards lockscreen. */
|
/** Hides blocked OneUI notification rows only on the collapsed cards lockscreen. */
|
||||||
internal class OneUiLockscreenCardsBackend(
|
internal class OneUiLockscreenCardsBackend(
|
||||||
@@ -18,6 +19,7 @@ internal class OneUiLockscreenCardsBackend(
|
|||||||
) : VisibilitySurfaceBackend {
|
) : VisibilitySurfaceBackend {
|
||||||
private val stacks = Collections.newSetFromMap(WeakHashMap<ViewGroup, Boolean>())
|
private val stacks = Collections.newSetFromMap(WeakHashMap<ViewGroup, Boolean>())
|
||||||
private val hiddenRows = WeakHashMap<View, RowState>()
|
private val hiddenRows = WeakHashMap<View, RowState>()
|
||||||
|
private val gridRenderer = OneUiCardsGridRenderer()
|
||||||
private lateinit var entryForRow: (View) -> Any?
|
private lateinit var entryForRow: (View) -> Any?
|
||||||
private lateinit var sbnForEntry: (Any) -> StatusBarNotification?
|
private lateinit var sbnForEntry: (Any) -> StatusBarNotification?
|
||||||
private lateinit var shelfNotification: Field
|
private lateinit var shelfNotification: Field
|
||||||
@@ -39,6 +41,7 @@ internal class OneUiLockscreenCardsBackend(
|
|||||||
val stateController = Class.forName(STATE_CONTROLLER, false, classLoader)
|
val stateController = Class.forName(STATE_CONTROLLER, false, classLoader)
|
||||||
prepareNotificationAccess(rowClass)
|
prepareNotificationAccess(rowClass)
|
||||||
installShelfFilter(classLoader)
|
installShelfFilter(classLoader)
|
||||||
|
installGridSourceHook(classLoader)
|
||||||
|
|
||||||
stackClass.declaredMethods.filter { it.name == "onViewAdded" }.forEach { method ->
|
stackClass.declaredMethods.filter { it.name == "onViewAdded" }.forEach { method ->
|
||||||
method.isAccessible = true
|
method.isAccessible = true
|
||||||
@@ -148,6 +151,49 @@ internal class OneUiLockscreenCardsBackend(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun installGridSourceHook(classLoader: ClassLoader) {
|
||||||
|
val controller = Class.forName(ICON_AREA_CONTROLLER, false, classLoader)
|
||||||
|
val shelfClass = Class.forName(SHELF_ICONS, false, classLoader)
|
||||||
|
controller.declaredMethods.filter { it.name == "updateIconsForLayout" }.forEach { method ->
|
||||||
|
method.isAccessible = true
|
||||||
|
framework.hook(method).intercept { chain ->
|
||||||
|
val functionIndex = chain.args.indexOfFirst { it is Function<*, *> }
|
||||||
|
val container = chain.args.firstOrNull { shelfClass.isInstance(it) } as? ViewGroup
|
||||||
|
if (functionIndex < 0 || container == null) {
|
||||||
|
return@intercept chain.proceed()
|
||||||
|
}
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val original = chain.args[functionIndex] as Function<Any?, Any?>
|
||||||
|
val sources = ArrayList<OneUiCardsGridRenderer.Source>()
|
||||||
|
val wrapped = Function<Any?, Any?> { entry ->
|
||||||
|
val result = original.apply(entry)
|
||||||
|
val sbn = entry?.let { runCatching { sbnForEntry(it) }.getOrNull() }
|
||||||
|
if (result is View && sbn != null && !isBlocked(sbn)) {
|
||||||
|
sources += OneUiCardsGridRenderer.Source(
|
||||||
|
sbn.key,
|
||||||
|
result,
|
||||||
|
sbn.notification.smallIcon,
|
||||||
|
sbn.packageName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
val arguments = chain.args.toTypedArray()
|
||||||
|
arguments[functionIndex] = wrapped
|
||||||
|
val result = chain.proceed(arguments)
|
||||||
|
gridRenderer.updateSources(container, sources)
|
||||||
|
val active = isLockscreenCards(container)
|
||||||
|
gridRenderer.refresh(
|
||||||
|
container,
|
||||||
|
active,
|
||||||
|
if (active) visibleCardKeys(container) else emptySet(),
|
||||||
|
gridConfig(),
|
||||||
|
)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun accessor(method: Method?, field: Field?): (Any) -> Any? {
|
private fun accessor(method: Method?, field: Field?): (Any) -> Any? {
|
||||||
method?.isAccessible = true
|
method?.isAccessible = true
|
||||||
field?.isAccessible = true
|
field?.isAccessible = true
|
||||||
@@ -165,6 +211,7 @@ internal class OneUiLockscreenCardsBackend(
|
|||||||
|
|
||||||
private fun applyTrackedStacks() {
|
private fun applyTrackedStacks() {
|
||||||
synchronized(stacks) { stacks.toList() }.forEach(::applyStack)
|
synchronized(stacks) { stacks.toList() }.forEach(::applyStack)
|
||||||
|
gridRenderer.refreshAll(::isLockscreenCards, ::visibleCardKeys, gridConfig())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyStack(stack: ViewGroup) {
|
private fun applyStack(stack: ViewGroup) {
|
||||||
@@ -250,6 +297,29 @@ internal class OneUiLockscreenCardsBackend(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun visibleCardKeys(container: ViewGroup): Set<String> {
|
||||||
|
val shelfLocation = IntArray(2)
|
||||||
|
runCatching { container.getLocationOnScreen(shelfLocation) }.getOrElse { return emptySet() }
|
||||||
|
val root = container.rootView
|
||||||
|
return synchronized(stacks) { stacks.toList() }
|
||||||
|
.asSequence()
|
||||||
|
.filter { it.rootView === root }
|
||||||
|
.flatMap { stack -> (0 until stack.childCount).asSequence().map(stack::getChildAt) }
|
||||||
|
.filter { row ->
|
||||||
|
row.javaClass.name == ROW && row.visibility == View.VISIBLE && row.alpha > 0.05f &&
|
||||||
|
!booleanField(row, "mInShelf") && !booleanField(row, "mTransformingInShelf")
|
||||||
|
}
|
||||||
|
.filter { row ->
|
||||||
|
val location = IntArray(2)
|
||||||
|
runCatching { row.getLocationOnScreen(location) }.isSuccess && location[1] < shelfLocation[1]
|
||||||
|
}
|
||||||
|
.mapNotNull { row -> runCatching { entryForRow(row)?.let(sbnForEntry)?.key }.getOrNull() }
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun booleanField(target: Any, name: String) =
|
||||||
|
runCatching { findField(target.javaClass, name)?.getBoolean(target) ?: false }.getOrDefault(false)
|
||||||
|
|
||||||
private fun requireField(type: Class<*>, name: String) =
|
private fun requireField(type: Class<*>, name: String) =
|
||||||
findField(type, name) ?: error("OneUI $name field is unavailable")
|
findField(type, name) ?: error("OneUI $name field is unavailable")
|
||||||
|
|
||||||
@@ -267,6 +337,12 @@ internal class OneUiLockscreenCardsBackend(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun gridConfig() = OneUiCardsGridRenderer.Config(
|
||||||
|
ProcessVisibilityPolicyCache.lockscreenCards,
|
||||||
|
showPills = true,
|
||||||
|
hideShelf = true,
|
||||||
|
)
|
||||||
|
|
||||||
private data class RowState(val visibility: Int, val alpha: Float)
|
private data class RowState(val visibility: Int, val alpha: Float)
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
@@ -282,6 +358,8 @@ internal class OneUiLockscreenCardsBackend(
|
|||||||
const val SHELF_ICONS = "com.android.systemui.statusbar.phone.SecShelfNotificationIconContainer"
|
const val SHELF_ICONS = "com.android.systemui.statusbar.phone.SecShelfNotificationIconContainer"
|
||||||
const val STATUS_BAR_ICON_VIEW = "com.android.systemui.statusbar.StatusBarIconView"
|
const val STATUS_BAR_ICON_VIEW = "com.android.systemui.statusbar.StatusBarIconView"
|
||||||
const val ICON_STATE = "com.android.systemui.statusbar.phone.NotificationIconContainer\$IconState"
|
const val ICON_STATE = "com.android.systemui.statusbar.phone.NotificationIconContainer\$IconState"
|
||||||
|
const val ICON_AREA_CONTROLLER =
|
||||||
|
"com.android.systemui.statusbar.phone.LegacyNotificationIconAreaControllerImpl"
|
||||||
const val ONE_UI_LOCKSCREEN_COORDINATOR =
|
const val ONE_UI_LOCKSCREEN_COORDINATOR =
|
||||||
"com.android.systemui.statusbar.notification.collection.coordinator.LockScreenNotiIconCoordinator"
|
"com.android.systemui.statusbar.notification.collection.coordinator.LockScreenNotiIconCoordinator"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="Max rows" />
|
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="Max rows" />
|
||||||
<Spinner android:id="@+id/max_rows" android:layout_width="wrap_content" android:layout_height="wrap_content" />
|
<Spinner android:id="@+id/max_rows" android:layout_width="wrap_content" android:layout_height="wrap_content" android:popupBackground="@drawable/dropdown_popup_background" />
|
||||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Max icons per row" />
|
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Max icons per row" />
|
||||||
<Spinner android:id="@+id/max_icons" android:layout_width="wrap_content" android:layout_height="wrap_content" />
|
<Spinner android:id="@+id/max_icons" android:layout_width="wrap_content" android:layout_height="wrap_content" android:popupBackground="@drawable/dropdown_popup_background" />
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/even_distribution" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Even distribution" />
|
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/even_distribution" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Even distribution" />
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/limit_width" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Limit to screen width" />
|
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/limit_width" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Limit to screen width" />
|
||||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:gravity="center_vertical" android:orientation="horizontal">
|
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:gravity="center_vertical" android:orientation="horizontal">
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="Max rows" />
|
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="Max rows" />
|
||||||
<Spinner android:id="@+id/max_rows" android:layout_width="wrap_content" android:layout_height="wrap_content" />
|
<Spinner android:id="@+id/max_rows" android:layout_width="wrap_content" android:layout_height="wrap_content" android:popupBackground="@drawable/dropdown_popup_background" />
|
||||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Max icons per row" />
|
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Max icons per row" />
|
||||||
<Spinner android:id="@+id/max_icons" android:layout_width="wrap_content" android:layout_height="wrap_content" />
|
<Spinner android:id="@+id/max_icons" android:layout_width="wrap_content" android:layout_height="wrap_content" android:popupBackground="@drawable/dropdown_popup_background" />
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/even_distribution" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Even distribution" />
|
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/even_distribution" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Even distribution" />
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/limit_width" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Limit to screen width" />
|
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/limit_width" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Limit to screen width" />
|
||||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:gravity="center_vertical" android:orientation="horizontal">
|
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:gravity="center_vertical" android:orientation="horizontal">
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
plugins { alias(libs.plugins.android.application); alias(libs.plugins.kotlin.android) }
|
plugins { alias(libs.plugins.android.application); alias(libs.plugins.kotlin.android) }
|
||||||
android { namespace = "se.ajpanton.notificationsmaster.helper"; compileSdk = 36
|
android { namespace = "se.ajpanton.notificationsmaster.helper"; compileSdk = 36
|
||||||
defaultConfig { applicationId = "se.ajpanton.notificationsmaster.helper"; minSdk = 34; targetSdk = 36; versionCode = 1; versionName = "0.1" }
|
defaultConfig {
|
||||||
|
applicationId = providers.gradleProperty("testHelperApplicationId")
|
||||||
|
.getOrElse("se.ajpanton.notificationsmaster.helper")
|
||||||
|
manifestPlaceholders["testHelperLabel"] = applicationId!!
|
||||||
|
minSdk = 34; targetSdk = 36; versionCode = 1; versionName = "0.1"
|
||||||
|
}
|
||||||
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
|
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
|
||||||
kotlin { compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) } }
|
kotlin { compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) } }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application android:theme="@style/Theme.AppCompat.DayNight">
|
<application android:label="${testHelperLabel}" android:theme="@style/Theme.AppCompat.DayNight">
|
||||||
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop">
|
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package se.ajpanton.notificationsmaster.helper
|
package se.ajpanton.notificationsmaster.helper
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
@@ -9,6 +10,7 @@ import android.graphics.Bitmap
|
|||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.RingtoneManager
|
import android.media.RingtoneManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.ScrollView
|
import android.widget.ScrollView
|
||||||
@@ -33,6 +35,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
vibrationPattern = longArrayOf(0, 200)
|
vibrationPattern = longArrayOf(0, 200)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
|
||||||
|
}
|
||||||
setContentView(ScrollView(this).apply {
|
setContentView(ScrollView(this).apply {
|
||||||
addView(LinearLayout(this@MainActivity).apply {
|
addView(LinearLayout(this@MainActivity).apply {
|
||||||
orientation = LinearLayout.VERTICAL
|
orientation = LinearLayout.VERTICAL
|
||||||
|
|||||||
Reference in New Issue
Block a user