Revert "feat(home): live glass blur behind icons, folders and widgets (#7393)" (#7419)

This reverts commit d9afe890a3.
This commit is contained in:
Pun Butrach authored and GitHub committed 2026-10-21 22:19:44 +07:00
1 parent d04b3b1c8d
commit eba9492be1
17 files changed
+5 -1092

No files matched your search

@@ -1,14 +1,5 @@
package app.lawnchair.root;
import android.os.Bundle;
interface IRootHelper {
oneway void goToSleep();
/**
* Creates an effect layer owned by root and returns it under the "layer" key. SurfaceFlinger
* never ignores the blur of layers created by privileged processes, while app layers lose
* all their blur past 10 blur requests on screen.
*/
Bundle createGlassBlurLayer();
}
-10
View File
@@ -330,16 +330,6 @@
<!-- Icon-related settings -->
<string name="transparent_background_icons_label">Transparent icon backgrounds</string>
<string name="glass_blur_label">Glass blur</string>
<string name="glass_blur_icons_label">Blur wallpaper behind icons</string>
<string name="glass_blur_icons_description">Live frosted glass behind home screen icons. Works best with translucent icons. Requires window blurs to be enabled, and root access to blur more than 10 items at once.</string>
<string name="glass_blur_radius_label">Blur radius</string>
<string name="glass_blur_tint_label">White tint</string>
<string name="glass_blur_folders_label">Folders</string>
<string name="glass_blur_folders_description">Folder icons and open folders. Folder colors and opacities set how dark the glass is</string>
<string name="glass_blur_widgets_label">Widgets</string>
<string name="glass_blur_widgets_description">Visible through translucent widgets</string>
<string name="auto_adaptive_icons_label">Auto-adaptive icons</string>
<string name="shadow_bg_icons_label">Show shadow behind icons</string>
<string name="background_lightness_label">Background lightness</string>
@@ -32,7 +32,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.lifecycleScope
import app.lawnchair.LawnchairApp.Companion.showQuickstepWarningIfNecessary
import app.lawnchair.blur.GlassBlur
import app.lawnchair.compat.LawnchairQuickstepCompat
import app.lawnchair.data.AppDatabase
import app.lawnchair.data.wallpaper.service.WallpaperService
@@ -168,9 +167,6 @@ class LawnchairLauncher : QuickstepLauncher() {
defaultOverlay.setEnableFeed(enable)
}.launchIn(scope = lifecycleScope)
launcher.stateManager.addStateListener(clearSearchStateListener)
if (prefs.glassBlurIcons.get()) {
launcher.stateManager.addStateListener(GlassBlur.createStateListener(this))
}
if (prefs.autoLaunchRoot.get()) {
lifecycleScope.launch {
@@ -1,216 +0,0 @@
package app.lawnchair.blur
import android.content.ContextWrapper
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Path
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.GradientDrawable
import android.view.View
import app.lawnchair.preferences.PreferenceManager
import com.android.launcher3.Launcher
import com.android.launcher3.LauncherState
import com.android.launcher3.ShortcutAndWidgetContainer
import com.android.launcher3.graphics.ThemeManager
import com.android.launcher3.statemanager.StateManager
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
/**
* Live blur of the wallpaper behind a home screen item, the Android equivalent of CSS
* backdrop-filter. Items draw it right before their own content: SurfaceFlinger blurs what is
* behind the launcher window within the region, which follows the item as it moves.
*/
class GlassBlur(private val view: View, private val type: Type) {
enum class Type { ICON, FOLDER_ICON, OPEN_FOLDER, WIDGET }
/**
* Workspace item drawing a glass blur, released while the item is hidden.
*/
interface Host {
fun releaseGlassBlur()
}
val isEnabled: Boolean
private val radiusPx: Int
// Folders and widgets get their tint from their own background
private val tint: Int
private var drawable: GlassBlurDrawable? = null
private val bounds = Rect()
private val clipBounds = RectF()
private val launcher by lazy {
generateSequence(view.context) { (it as? ContextWrapper)?.baseContext }
.firstOrNull { it is Launcher } as Launcher?
}
init {
// The launcher is recreated when these settings change
val prefs = PreferenceManager.getInstance(view.context)
isEnabled = prefs.glassBlurIcons.get() && when (type) {
Type.ICON -> true
Type.FOLDER_ICON, Type.OPEN_FOLDER -> prefs.glassBlurFolders.get()
Type.WIDGET -> prefs.glassBlurWidgets.get()
}
radiusPx = (prefs.glassBlurRadius.get() * view.resources.displayMetrics.density).roundToInt()
tint = if (type == Type.ICON) {
Color.argb((prefs.glassBlurTint.get() * 255).roundToInt(), 255, 255, 255)
} else {
Color.TRANSPARENT
}
}
/**
* Blurs behind the opaque part of an icon bitmap drawn within the given bounds.
*/
fun drawIcon(canvas: Canvas, iconBounds: Rect, icon: Bitmap) {
if (!canDraw(canvas)) {
return
}
val shape = GlassBlurShapes.getIconShape(icon, view) ?: return
val size = iconBounds.width()
// Slightly inside the icon, so the edge of the blur never shows around it
val inset = (size * ICON_INSET_FRACTION).roundToInt()
bounds.set(
iconBounds.left + (shape.bounds.left * size).roundToInt() + inset,
iconBounds.top + (shape.bounds.top * size).roundToInt() + inset,
iconBounds.left + (shape.bounds.right * size).roundToInt() - inset,
iconBounds.top + (shape.bounds.bottom * size).roundToInt() - inset,
)
draw(canvas, shape.radiusTL * size, shape.radiusTR * size, shape.radiusBL * size, shape.radiusBR * size)
}
/**
* Blurs behind a folder icon background, drawn with the folder shape at the given position.
*/
fun drawFolderIcon(canvas: Canvas, left: Int, top: Int, radius: Int) {
if (view.parent !is ShortcutAndWidgetContainer || !canDraw(canvas)) {
return
}
bounds.set(left, top, left + 2 * radius, top + 2 * radius)
val folderShape = ThemeManager.INSTANCE.get(view.context).folderShape
val corner = radius * GlassBlurShapes.getFolderCornerRatio(folderShape)
draw(canvas, corner, corner, corner, corner)
}
/**
* Blurs behind an open folder background. While opening and closing, the folder is clipped
* to a rounded rect growing from the folder icon, which the blur follows.
*/
fun drawOpenFolder(canvas: Canvas, background: GradientDrawable, clipPath: Path?) {
if (!canDraw(canvas)) {
return
}
bounds.set(background.bounds)
var corner = background.cornerRadius
if (clipPath != null) {
clipPath.computeBounds(clipBounds, false)
val intersects = bounds.intersect(
clipBounds.left.roundToInt(),
clipBounds.top.roundToInt(),
clipBounds.right.roundToInt(),
clipBounds.bottom.roundToInt(),
)
if (!intersects) {
bounds.setEmpty()
}
// The reveal starts from the folder icon, about as round as a circle
corner = min(corner, min(bounds.width(), bounds.height()) / 2f)
}
draw(canvas, corner, corner, corner, corner)
}
/**
* Blurs behind a widget background, with the corners the widget is clipped to.
*/
fun drawWidget(canvas: Canvas, backgroundBounds: Rect, cornerRadius: Float) {
if (backgroundBounds.isEmpty || view.parent !is ShortcutAndWidgetContainer || !canDraw(canvas)) {
return
}
bounds.set(backgroundBounds)
val corner = max(cornerRadius, 0f)
draw(canvas, corner, corner, corner, corner)
}
/**
* Unregisters the blur region, which is registered again on the next draw.
*/
fun release() {
drawable?.setVisible(false, false)
drawable = null
}
private fun canDraw(canvas: Canvas): Boolean {
// Software canvases (e.g. icon snapshots) cannot draw render nodes
if (!isEnabled || !canvas.isHardwareAccelerated) {
return false
}
// Folders opened from the app drawer are not hidden by it
val state = launcher?.stateManager?.state
return type == Type.OPEN_FOLDER || state == null || !isSuppressedIn(state)
}
private fun draw(canvas: Canvas, radiusTL: Float, radiusTR: Float, radiusBL: Float, radiusBR: Float) {
val blur = drawable ?: run {
if (view.viewRootImpl == null) {
return
}
GlassBlurDrawable.create(view).also {
it.setBlurRadius(radiusPx)
it.setColor(tint)
drawable = it
}
}
blur.bounds = bounds
blur.setCornerRadius(radiusTL, radiusTR, radiusBL, radiusBR)
blur.draw(canvas)
}
companion object {
private const val ICON_INSET_FRACTION = 0.02f
/**
* Workspace items are hidden behind the app drawer and recents. Without root, their
* regions would also count towards the SurfaceFlinger limit of 10 blur requests on
* screen, and disable the window background blur used by the drawer and recents.
*/
private fun isSuppressedIn(state: LauncherState) = state == LauncherState.ALL_APPS || state.isRecentsViewVisible
/**
* Releases the blur regions of the workspace items while they are hidden.
*/
@JvmStatic
fun createStateListener(launcher: Launcher) = object : StateManager.StateListener<LauncherState> {
override fun onStateTransitionStart(toState: LauncherState) {
if (isSuppressedIn(toState)) {
releaseWorkspaceItems(launcher)
}
}
override fun onStateTransitionComplete(finalState: LauncherState) {
if (isSuppressedIn(finalState)) {
releaseWorkspaceItems(launcher)
} else {
// Items are not redrawn on their own, while their regions are registered on draw
launcher.workspace.mapOverItems { _, item ->
if (item is Host) {
item.invalidate()
}
false
}
}
}
}
private fun releaseWorkspaceItems(launcher: Launcher) {
launcher.workspace.mapOverItems { _, item ->
(item as? Host)?.releaseGlassBlur()
false
}
}
}
}
@@ -1,466 +0,0 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.lawnchair.blur
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.graphics.RenderNode
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.ArraySet
import android.util.LongSparseArray
import android.view.SurfaceControl
import android.view.View
import android.view.ViewRootImpl
import android.view.ViewTreeObserver
import androidx.annotation.UiThread
import java.util.WeakHashMap
/**
* Copy of the framework BackgroundBlurDrawable that sends its blur regions to an effect layer
* created by root (see [GlassBlurRootLayer]), placed right below the window content.
* SurfaceFlinger ignores all the blur of an app layer past 10 blur requests on screen
* (b/399120953), but not the blur of layers created by privileged processes. Without root, the
* regions go to the window layer, like with the framework drawable.
*/
class GlassBlurDrawable private constructor(private val aggregator: Aggregator) : Drawable() {
private val renderNode = RenderNode("GlassBlurDrawable")
private val paint = Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
color = Color.TRANSPARENT
isAntiAlias = true
}
private val rectPath = Path()
private val tmpRadii = FloatArray(8)
// Confined to the UI thread. The values are copied into a BlurRegion, which lives on
// RenderThread to avoid interference with UI thread updates.
private var regionVisible = true
private var blurRadius = 0
private var cornerRadiusTL = 0f
private var cornerRadiusTR = 0f
private var cornerRadiusBL = 0f
private var cornerRadiusBR = 0f
private var regionAlpha = 255
// Do not update from the UI thread. This holds the latest position for this drawable. It is
// used by the Aggregator from RenderThread to get the final position of the region sent to SF
private val rect = Rect()
// HWUI only keeps a weak reference to the listener. It is called from a thread pool, and the
// callbacks might come out of order w.r.t. the frame number, so the Aggregator applies the
// updates on RenderThread when processing that same frame.
private val positionUpdateListener = object : RenderNode.PositionUpdateListener {
override fun positionChanged(frameNumber: Long, left: Int, top: Int, right: Int, bottom: Int) {
aggregator.onRenderNodePositionChanged(frameNumber) { rect.set(left, top, right, bottom) }
}
override fun positionLost(frameNumber: Long) {
aggregator.onRenderNodePositionChanged(frameNumber) { rect.setEmpty() }
}
}
init {
renderNode.addPositionUpdateListener(positionUpdateListener)
}
override fun draw(canvas: Canvas) {
if (rectPath.isEmpty || !isVisible || regionAlpha == 0) {
return
}
canvas.drawPath(rectPath, paint)
canvas.drawRenderNode(renderNode)
}
/**
* Color that will be alpha blended on top of the blur.
*/
fun setColor(color: Int) {
paint.color = color
}
override fun setVisible(visible: Boolean, restart: Boolean): Boolean {
val changed = super.setVisible(visible, restart)
if (changed) {
regionVisible = visible
aggregator.onBlurDrawableUpdated(this)
}
return changed
}
override fun setAlpha(alpha: Int) {
if (regionAlpha != alpha) {
regionAlpha = alpha
invalidateSelf()
aggregator.onBlurDrawableUpdated(this)
}
}
override fun getAlpha() = regionAlpha
/**
* Blur radius in pixels.
*/
fun setBlurRadius(radius: Int) {
if (blurRadius != radius) {
blurRadius = radius
invalidateSelf()
aggregator.onBlurDrawableUpdated(this)
}
}
/**
* Sets the radius of each corner, in pixels.
*/
fun setCornerRadius(topLeft: Float, topRight: Float, bottomLeft: Float, bottomRight: Float) {
if (cornerRadiusTL != topLeft || cornerRadiusTR != topRight ||
cornerRadiusBL != bottomLeft || cornerRadiusBR != bottomRight
) {
cornerRadiusTL = topLeft
cornerRadiusTR = topRight
cornerRadiusBL = bottomLeft
cornerRadiusBR = bottomRight
updatePath()
invalidateSelf()
aggregator.onBlurDrawableUpdated(this)
}
}
override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
super.setBounds(left, top, right, bottom)
renderNode.setPosition(left, top, right, bottom)
updatePath()
}
private fun updatePath() {
// Clockwise from the top left corner
tmpRadii.fill(cornerRadiusTL, 0, 2)
tmpRadii.fill(cornerRadiusTR, 2, 4)
tmpRadii.fill(cornerRadiusBR, 4, 6)
tmpRadii.fill(cornerRadiusBL, 6, 8)
rectPath.reset()
if (regionAlpha == 0 || !isVisible) {
return
}
val b = bounds
rectPath.addRoundRect(
b.left.toFloat(),
b.top.toFloat(),
b.right.toFloat(),
b.bottom.toFloat(),
tmpRadii,
Path.Direction.CW,
)
}
override fun setColorFilter(colorFilter: ColorFilter?) {
throw IllegalArgumentException("not implemented")
}
@Deprecated("Deprecated in Java")
override fun getOpacity() = PixelFormat.TRANSLUCENT
/**
* Keeps track of all the blur regions of a [ViewRootImpl] and sends them to SurfaceFlinger
* along with the frame they belong to.
*/
private class Aggregator(private val viewRoot: ViewRootImpl) : ViewRootImpl.SurfaceChangedCallback {
private val rtLock = Any()
// All the visible blur drawables, confined to the UI thread
private val drawables = ArraySet<GlassBlurDrawable>()
// Guarded by rtLock
private val frameRtUpdates = LongSparseArray<ArraySet<Runnable>>()
private var lastFrameNumber = 0L
private var lastFrameBlurRegions: Array<BlurRegion>? = null
private var tmpBlurRegionsForFrame = emptyArray<BlurRegion>()
private var hasUiUpdates = false
private var onPreDrawListener: ViewTreeObserver.OnPreDrawListener? = null
// Written on the UI thread, read on RenderThread
@Volatile
private var rootLayer: SurfaceControl? = null
// Regions sent to the window layer before the root layer arrived have to be cleared
@Volatile
private var clearWindowRegions = false
@UiThread
private fun onRootLayerReady(layer: SurfaceControl) {
val rootView = viewRoot.view
if (rootView == null || !rootView.isAttachedToWindow) {
SurfaceControl.Transaction().remove(layer).apply()
return
}
rootLayer = layer
clearWindowRegions = true
viewRoot.addSurfaceChangedCallback(this)
rootView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {}
override fun onViewDetachedFromWindow(v: View) {
v.removeOnAttachStateChangeListener(this)
viewRoot.removeSurfaceChangedCallback(this@Aggregator)
aggregators.remove(viewRoot)
rootLayer = null
SurfaceControl.Transaction().remove(layer).apply()
}
})
val t = SurfaceControl.Transaction()
attachRootLayer(t)
t.apply()
if (hasRegions()) {
hasUiUpdates = true
rootView.invalidate()
}
}
private fun attachRootLayer(t: SurfaceControl.Transaction) {
val layer = rootLayer ?: return
val windowLayer = viewRoot.surfaceControl
if (!windowLayer.isValid) {
return
}
// Below the window content, so it blurs what is behind the window (the wallpaper)
t.reparent(layer, windowLayer).setLayer(layer, -1).show(layer)
}
override fun surfaceCreated(t: SurfaceControl.Transaction) {
attachRootLayer(t)
}
override fun surfaceReplaced(t: SurfaceControl.Transaction) {
attachRootLayer(t)
}
override fun surfaceDestroyed() {}
@UiThread
fun onBlurDrawableUpdated(drawable: GlassBlurDrawable) {
val shouldBeDrawn = drawable.regionAlpha > 0 && drawable.blurRadius > 0 &&
drawable.regionVisible
val isDrawn = drawables.contains(drawable)
if (shouldBeDrawn) {
hasUiUpdates = true
if (!isDrawn) {
drawables.add(drawable)
}
} else if (isDrawn) {
hasUiUpdates = true
drawables.remove(drawable)
// The pre-draw listener only sends the regions left, even none, if a frame is drawn
if (onPreDrawListener != null) {
viewRoot.view?.invalidate()
}
}
val rootView = viewRoot.view
if (onPreDrawListener == null && rootView != null && hasRegions()) {
registerPreDrawListener(rootView)
if (hasUiUpdates) {
rootView.invalidate()
}
}
}
private fun registerPreDrawListener(rootView: View) {
val listener = ViewTreeObserver.OnPreDrawListener {
val uiUpdates = hasUiUpdates
if (uiUpdates || hasRegions()) {
val blurRegionsForNextFrame = getBlurRegionsCopyForRT()
viewRoot.registerRtFrameCallback { frame ->
synchronized(rtLock) {
lastFrameNumber = frame
lastFrameBlurRegions = blurRegionsForNextFrame
handleDispatchBlurTransactionLocked(frame, blurRegionsForNextFrame, uiUpdates)
}
}
}
if (!hasRegions()) {
viewRoot.view?.viewTreeObserver?.removeOnPreDrawListener(onPreDrawListener)
onPreDrawListener = null
}
true
}
onPreDrawListener = listener
rootView.viewTreeObserver.addOnPreDrawListener(listener)
}
// Called from a thread pool
fun onRenderNodePositionChanged(frameNumber: Long, update: Runnable) {
// One of the blur region's position has changed, so we have to send an updated list
// of blur regions to SurfaceFlinger for this frame.
synchronized(rtLock) {
val updates = frameRtUpdates.get(frameNumber)
?: ArraySet<Runnable>().also { frameRtUpdates.put(frameNumber, it) }
updates.add(update)
if (lastFrameNumber == frameNumber) {
// The transaction for this frame has already been sent, so we have to manually
// trigger sending a transaction here in order to apply this position update
lastFrameBlurRegions?.let {
handleDispatchBlurTransactionLocked(frameNumber, it, true)
}
}
}
}
@UiThread
private fun hasRegions() = drawables.isNotEmpty()
@UiThread
private fun getBlurRegionsCopyForRT(): Array<BlurRegion> {
if (hasUiUpdates) {
tmpBlurRegionsForFrame = Array(drawables.size) { BlurRegion(drawables.valueAt(it)) }
hasUiUpdates = false
}
return tmpBlurRegionsForFrame
}
/**
* Called on RenderThread with rtLock held.
*
* @return the regions to send to SurfaceFlinger for this frame, or null if none changed
*/
private fun getBlurRegionsForFrameLocked(
frameNumber: Long,
blurRegionsForFrame: Array<BlurRegion>,
forceUpdate: Boolean,
): Array<FloatArray>? {
if (!forceUpdate && (frameRtUpdates.size() == 0 || frameRtUpdates.keyAt(0) > frameNumber)) {
return null
}
// frameRtUpdates holds position updates coming from a thread pool span from
// RenderThread. At this point, all position updates for frame frameNumber should
// have been added to frameRtUpdates.
// Here, we apply all updates for frames <= frameNumber in case some previous update
// has been missed. This also protects frameRtUpdates from memory leaks.
while (frameRtUpdates.size() != 0 && frameRtUpdates.keyAt(0) <= frameNumber) {
val frameUpdates = frameRtUpdates.valueAt(0)
frameRtUpdates.removeAt(0)
frameUpdates.forEach { it.run() }
}
return blurRegionsForFrame
.filter { !it.rect.isEmpty }
.map { it.toFloatArray() }
.toTypedArray()
}
/**
* Dispatches all blur regions if there are any UI or position updates for that frame.
*/
private fun handleDispatchBlurTransactionLocked(
frameNumber: Long,
blurRegions: Array<BlurRegion>,
forceUpdate: Boolean,
) {
val regions = getBlurRegionsForFrameLocked(frameNumber, blurRegions, forceUpdate)
?: return
val layer = rootLayer
if (layer == null) {
viewRoot.dispatchBlurRegions(regions, frameNumber)
return
}
val t = SurfaceControl.Transaction()
t.setBlurRegions(layer, regions)
if (clearWindowRegions) {
clearWindowRegions = false
val windowLayer = viewRoot.surfaceControl
if (windowLayer.isValid) {
t.setBlurRegions(windowLayer, emptyArray())
}
}
viewRoot.mergeWithNextTransaction(t, frameNumber)
}
companion object {
@UiThread
fun get(viewRoot: ViewRootImpl, context: Context): Aggregator = aggregators.getOrPut(viewRoot) {
Aggregator(viewRoot).also { aggregator ->
GlassBlurRootLayer.request(context, aggregator::onRootLayerReady)
}
}
}
}
/**
* Snapshot of a blur drawable, confined to RenderThread.
*/
private class BlurRegion(drawable: GlassBlurDrawable) {
private val blurRadius = drawable.blurRadius
private val cornerRadiusTL = drawable.cornerRadiusTL
private val cornerRadiusTR = drawable.cornerRadiusTR
private val cornerRadiusBL = drawable.cornerRadiusBL
private val cornerRadiusBR = drawable.cornerRadiusBR
private val alpha = drawable.regionAlpha / 255f
val rect = drawable.rect
/**
* Serializes this region into the layout expected by
* SurfaceControl.Transaction#setBlurRegions, which has separate X/Y corner radii since
* Android 17.
*/
fun toFloatArray(): FloatArray {
val position = floatArrayOf(
blurRadius.toFloat(),
alpha,
rect.left.toFloat(),
rect.top.toFloat(),
rect.right.toFloat(),
rect.bottom.toFloat(),
)
val corners = if (Build.VERSION.SDK_INT >= 37) {
floatArrayOf(
cornerRadiusTL,
cornerRadiusTL,
cornerRadiusTR,
cornerRadiusTR,
cornerRadiusBL,
cornerRadiusBL,
cornerRadiusBR,
cornerRadiusBR,
)
} else {
floatArrayOf(cornerRadiusTL, cornerRadiusTR, cornerRadiusBL, cornerRadiusBR)
}
return position + corners
}
}
companion object {
private val aggregators = WeakHashMap<ViewRootImpl, Aggregator>()
/**
* Creates a blur region for the window of the given view, which must be attached.
*/
@UiThread
fun create(view: View): GlassBlurDrawable = GlassBlurDrawable(Aggregator.get(view.viewRootImpl, view.context))
}
}
@@ -1,56 +0,0 @@
package app.lawnchair.blur
import android.content.Context
import android.os.RemoteException
import android.util.Log
import android.view.SurfaceControl
import androidx.annotation.UiThread
import app.lawnchair.root.RootHelperBackend
import app.lawnchair.root.RootHelperManager
import app.lawnchair.root.RootNotAvailableException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
/**
* Hands out effect layers created by the root helper. SurfaceFlinger never ignores the blur of
* layers created by privileged processes, so their blur regions are exempt from the limit of 10
* blur requests that applies to app layers.
*/
internal object GlassBlurRootLayer {
private const val TAG = "LC-GlassBlur"
private val scope = MainScope() + CoroutineName("GlassBlurRootLayer")
/**
* Calls back on the main thread with a new layer. Never calls back without root.
*/
@UiThread
fun request(context: Context, callback: (SurfaceControl) -> Unit) {
val rootHelperManager = RootHelperManager.INSTANCE.get(context)
scope.launch {
try {
if (!rootHelperManager.isAvailable()) {
Log.i(TAG, "Root not available, blur limited to 10 regions on screen")
return@launch
}
val rootHelper = rootHelperManager.getService()
val layer = withContext(Dispatchers.IO) {
rootHelper.createGlassBlurLayer().getParcelable(
RootHelperBackend.GLASS_BLUR_LAYER_KEY,
SurfaceControl::class.java,
)
}
if (layer != null) {
callback(layer)
}
} catch (_: RootNotAvailableException) {
} catch (e: RemoteException) {
Log.w(TAG, "Unable to create the blur layer", e)
}
}
}
}
@@ -1,145 +0,0 @@
package app.lawnchair.blur
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Region
import android.view.View
import androidx.annotation.UiThread
import com.android.launcher3.graphics.ShapeDelegate
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR
import java.lang.ref.WeakReference
import java.util.WeakHashMap
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
/**
* Shapes of the blur regions. SurfaceFlinger only supports rounded rects, with a radius per
* corner, so these are the closest fit to the actual shapes.
*/
internal object GlassBlurShapes {
// Icon shape pixels are at least this opaque, translucent icon packs go down to ~0.2 alpha...
private const val ICON_ALPHA_THRESHOLD = 32
// ...and not black, to leave out the shadow drawn around icon pack icons
private const val ICON_MIN_CHANNEL = 24
// On a corner rounded with radius r, the first point of the diagonal inside the shape is
// r * (1 - 1/sqrt(2)) away from the corner along each axis
private val DIAGONAL_TO_RADIUS = 1f / (1f - sqrt(0.5f))
/**
* Blur region of an icon: the bounding box of its opaque part and the radius of each
* corner, as fractions of the bitmap width.
*/
class IconShape(val bounds: RectF, val radiusTL: Float, val radiusTR: Float, val radiusBL: Float, val radiusBR: Float)
// Confined to the UI thread
private val iconShapes = WeakHashMap<Bitmap, IconShape>()
private val viewsWaitingForShape = WeakHashMap<Bitmap, MutableList<WeakReference<View>>>()
private var folderShape: ShapeDelegate? = null
private var folderCornerRatio = 0f
/**
* Returns the blur region of an icon. The first time, it returns null and reads the pixels
* in the background, then redraws the view.
*/
@UiThread
fun getIconShape(icon: Bitmap, view: View): IconShape? {
iconShapes[icon]?.let { return it }
viewsWaitingForShape[icon]?.let {
it.add(WeakReference(view))
return null
}
viewsWaitingForShape[icon] = mutableListOf(WeakReference(view))
THREAD_POOL_EXECUTOR.execute {
val shape = computeIconShape(icon)
MAIN_EXECUTOR.execute {
iconShapes[icon] = shape
viewsWaitingForShape.remove(icon)?.forEach { it.get()?.invalidate() }
}
}
return null
}
private fun computeIconShape(icon: Bitmap): IconShape {
// Hardware bitmaps cannot be read directly
val readable = if (icon.config == Bitmap.Config.HARDWARE) {
icon.copy(Bitmap.Config.ARGB_8888, false)
} else {
icon
}
val w = readable.width
val h = readable.height
val pixels = IntArray(w * h)
readable.getPixels(pixels, 0, w, 0, 0, w, h)
if (readable !== icon) {
readable.recycle()
}
var left = w
var top = h
var right = -1
var bottom = -1
for (y in 0 until h) {
for (x in 0 until w) {
if (isShapePixel(pixels[y * w + x])) {
left = min(left, x)
right = max(right, x)
top = min(top, y)
bottom = max(bottom, y)
}
}
}
if (right < 0) {
return IconShape(RectF(), 0f, 0f, 0f, 0f)
}
val maxRadius = min(right - left + 1, bottom - top + 1) / 2
fun cornerRadius(x: Int, y: Int, dx: Int, dy: Int): Float {
for (k in 0 until maxRadius) {
if (isShapePixel(pixels[(y + dy * k) * w + x + dx * k])) {
return min(k * DIAGONAL_TO_RADIUS, maxRadius.toFloat()) / w
}
}
return maxRadius.toFloat() / w
}
return IconShape(
RectF(left.toFloat() / w, top.toFloat() / h, (right + 1).toFloat() / w, (bottom + 1).toFloat() / h),
radiusTL = cornerRadius(left, top, 1, 1),
radiusTR = cornerRadius(right, top, -1, 1),
radiusBL = cornerRadius(left, bottom, 1, -1),
radiusBR = cornerRadius(right, bottom, -1, -1),
)
}
private fun isShapePixel(argb: Int) = Color.alpha(argb) >= ICON_ALPHA_THRESHOLD &&
max(Color.red(argb), max(Color.green(argb), Color.blue(argb))) >= ICON_MIN_CHANNEL
/**
* Returns the corner radius of the folder shape, as a fraction of half its edge.
*/
@UiThread
fun getFolderCornerRatio(shape: ShapeDelegate): Float {
if (shape == folderShape) {
return folderCornerRatio
}
val ratio = if (shape is ShapeDelegate.RoundedSquare) {
shape.radiusRatio
} else {
val path = Path().also { shape.addToPath(it, 0f, 0f, 50f) }
val region = Region().apply { setPath(path, Region(0, 0, 100, 100)) }
var k = 0
while (k < 50 && !region.contains(k, k)) {
k++
}
min(1f, k * DIAGONAL_TO_RADIUS / 50f)
}
folderShape = shape
folderCornerRatio = ratio
return ratio
}
}
@@ -76,11 +76,6 @@ class PreferenceManager @Inject constructor(
val wrapAdaptiveIcons = BoolPref("prefs_wrapAdaptive", true)
val transparentIconBackground = BoolPref("prefs_transparentIconBackground", false)
val shadowBGIcons = BoolPref("pref_shadowBGIcons", true)
val glassBlurIcons = BoolPref("pref_glassBlurIcons", false, recreate)
val glassBlurRadius = IntPref("pref_glassBlurRadius", 24, recreate)
val glassBlurTint = FloatPref("pref_glassBlurTint", 0.2f, recreate)
val glassBlurFolders = BoolPref("pref_glassBlurFolders", true, recreate)
val glassBlurWidgets = BoolPref("pref_glassBlurWidgets", true, recreate)
val addIconToHome = BoolPref("pref_add_icon_to_home", true)
private val isPhone: Boolean get() = deviceType == InvariantDeviceProfile.TYPE_PHONE
@@ -1,24 +1,13 @@
package app.lawnchair.root;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemClock;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import androidx.core.content.ContextCompat;
public class RootHelperBackend extends IRootHelper.Stub {
public static final String GLASS_BLUR_LAYER_KEY = "layer";
// The client holds its own reference to the layer once the reply is parceled
private static final long GLASS_BLUR_LAYER_RELEASE_DELAY_MS = 10_000;
private final Context mContext;
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
public RootHelperBackend(Context context) {
mContext = context;
@@ -32,17 +21,4 @@ public class RootHelperBackend extends IRootHelper.Stub {
public void goToSleep() {
getPowerManager().goToSleep(SystemClock.uptimeMillis());
}
@Override
public Bundle createGlassBlurLayer() {
// SurfaceFlinger records the permissions of the creating process on the layer
SurfaceControl layer = new SurfaceControl.Builder(new SurfaceSession())
.setName("LawnchairGlassBlur")
.setEffectLayer()
.build();
Bundle result = new Bundle();
result.putParcelable(GLASS_BLUR_LAYER_KEY, layer);
mMainHandler.postDelayed(layer::release, GLASS_BLUR_LAYER_RELEASE_DELAY_MS);
return result;
}
}
@@ -24,8 +24,7 @@ class RootHelperManager(private val context: Context) {
private val prefs = PreferenceManager.getInstance(context)
private val isAvailableDeferred by lazy {
scope.async(Dispatchers.IO) {
// Shell.rootAccess() is false until a root shell has been created
Shell.getShell().isRoot
Shell.rootAccess()
}
}
private var rootHelperDeferred: Deferred<IRootHelper>? = null
@@ -47,8 +46,7 @@ class RootHelperManager(private val context: Context) {
}
private suspend fun bindImpl(onDisconnected: () -> Unit): IRootHelper {
// RootService.bind() can only be called on the main thread
return withContext(Dispatchers.Main) {
return withContext(Dispatchers.IO) {
val intent = Intent(context, RootHelper::class.java)
suspendCoroutine {
val connection = object : ServiceConnection {
@@ -160,48 +160,6 @@ fun GeneralPreferences(modifier: Modifier = Modifier) {
)
}
val glassBlurIcons = prefs.glassBlurIcons.getAdapter()
PreferenceGroup(heading = stringResource(id = R.string.glass_blur_label)) {
SwitchPreference(
adapter = glassBlurIcons,
label = stringResource(id = R.string.glass_blur_icons_label),
description = stringResource(id = R.string.glass_blur_icons_description),
)
ExpandAndShrink(visible = glassBlurIcons.state.value) {
SliderPreference(
label = stringResource(id = R.string.glass_blur_radius_label),
adapter = prefs.glassBlurRadius.getAdapter(),
step = 2,
valueRange = 4..64,
showUnit = "dp",
)
}
ExpandAndShrink(visible = glassBlurIcons.state.value) {
SliderPreference(
label = stringResource(id = R.string.glass_blur_tint_label),
adapter = prefs.glassBlurTint.getAdapter(),
valueRange = 0F..0.6F,
step = 0.05f,
showAsPercentage = true,
)
}
ExpandAndShrink(visible = glassBlurIcons.state.value) {
SwitchPreference(
adapter = prefs.glassBlurFolders.getAdapter(),
label = stringResource(id = R.string.glass_blur_folders_label),
description = stringResource(id = R.string.glass_blur_folders_description),
)
}
ExpandAndShrink(visible = glassBlurIcons.state.value) {
SwitchPreference(
adapter = prefs.glassBlurWidgets.getAdapter(),
label = stringResource(id = R.string.glass_blur_widgets_label),
description = stringResource(id = R.string.glass_blur_widgets_description),
)
}
}
val accentColorAdapter = prefs2.accentColor.getAdapter()
val showColorStyle = !(Utilities.ATLEAST_S && accentColorAdapter.state.value == ColorOption.SystemAccent) ||
!Utilities.ATLEAST_S
+1 -38
View File
@@ -117,7 +117,6 @@ import app.lawnchair.preferences.PreferenceManager;
import app.lawnchair.preferences2.PreferenceManager2;
import app.lawnchair.util.LawnchairUtilsKt;
import app.lawnchair.animation.PhysicsAnimator;
import app.lawnchair.blur.GlassBlur;
/**
* TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
@@ -125,7 +124,7 @@ import app.lawnchair.blur.GlassBlur;
* too aggressive.
*/
public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
FloatingIconViewCompanion, DraggableView, Reorderable, GlassBlur.Host {
FloatingIconViewCompanion, DraggableView, Reorderable {
public static final String TAG = "BubbleTextView";
@@ -225,10 +224,6 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
private final Rect mRunningAppIconBounds = new Rect();
private RunningAppState mRunningAppState;
// LC-Note: live blur of the wallpaper behind workspace icons
private final GlassBlur mGlassBlur = new GlassBlur(this, GlassBlur.Type.ICON);
private final Rect mGlassBlurBounds = new Rect();
@ViewDebug.ExportedProperty(category = "launcher")
private int mLineIndicatorColor;
@ViewDebug.ExportedProperty(category = "launcher")
@@ -815,9 +810,6 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
if (mIcon != null) {
mIcon.setVisible(isVisible, false);
}
if (!isVisible) {
releaseGlassBlur();
}
}
@Override
@@ -929,40 +921,11 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
@Override
public void onDraw(Canvas canvas) {
drawGlassBlurIfNecessary(canvas);
super.onDraw(canvas);
drawDotIfNecessary(canvas);
drawRunningAppIndicatorIfNecessary(canvas);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
releaseGlassBlur();
}
@Override
public void releaseGlassBlur() {
mGlassBlur.release();
}
/**
* LC-Note: draws the blur of the wallpaper behind the icon, before the icon itself.
*/
protected void drawGlassBlurIfNecessary(Canvas canvas) {
if (!mGlassBlur.isEnabled() || mDisplay != DISPLAY_WORKSPACE || mIcon == null
|| !mIsIconVisible) {
return;
}
getIconBounds(mGlassBlurBounds);
// The canvas is translated by the scroll position, unlike the icon bounds
int scrollX = getScrollX();
int scrollY = getScrollY();
canvas.translate(scrollX, scrollY);
mGlassBlur.drawIcon(canvas, mGlassBlurBounds, mIcon.bitmapInfo.icon);
canvas.translate(-scrollX, -scrollY);
}
/**
* Draws the notification dot in the top right corner of the icon bounds.
*
@@ -138,7 +138,6 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.patrykmichalik.opto.core.PreferenceExtensionsKt;
import app.lawnchair.blur.GlassBlur;
import app.lawnchair.preferences2.PreferenceManager2;
import app.lawnchair.theme.color.ColorOption;
import app.lawnchair.theme.color.tokens.ColorTokens;
@@ -297,9 +296,6 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
private @NonNull GradientDrawable mBackground;
// LC-Note: live blur of the wallpaper behind the folder background
private final GlassBlur mGlassBlur = new GlassBlur(this, GlassBlur.Type.OPEN_FOLDER);
PreferenceManager2 preferenceManager2;
/**
@@ -662,7 +658,6 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mFolderName.removeOnFocusChangeListener(this);
mGlassBlur.release();
}
@Override
@@ -2051,8 +2046,6 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
@Override
protected void dispatchDraw(Canvas canvas) {
// LC-Note: the blur follows the clip path of the reveal animation
mGlassBlur.drawOpenFolder(canvas, mBackground, mClipPath);
if (mClipPath != null) {
int count = canvas.save();
canvas.clipPath(mClipPath);
@@ -92,13 +92,11 @@ import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import app.lawnchair.blur.GlassBlur;
/**
* An icon that can appear on in the workspace representing an {@link Folder}.
*/
public class FolderIcon extends FrameLayout implements FloatingIconViewCompanion,
DraggableView, Reorderable, GlassBlur.Host {
DraggableView, Reorderable {
private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this);
@Thunk ActivityContext mActivity;
@@ -120,9 +118,6 @@ public class FolderIcon extends FrameLayout implements FloatingIconViewCompanion
PreviewBackground mBackground = new PreviewBackground(getContext());
private boolean mBackgroundIsVisible = true;
// LC-Note: live blur of the wallpaper behind the folder icon background
private final GlassBlur mGlassBlur = new GlassBlur(this, GlassBlur.Type.FOLDER_ICON);
FolderGridOrganizer mPreviewVerifier;
ClippedFolderIconLayoutRule mPreviewLayoutRule;
private PreviewItemManager mPreviewItemManager;
@@ -606,8 +601,6 @@ public class FolderIcon extends FrameLayout implements FloatingIconViewCompanion
mPreviewItemManager.recomputePreviewDrawingParams();
if (!mBackground.drawingDelegated()) {
mGlassBlur.drawFolderIcon(canvas, mBackground.getOffsetX(), mBackground.getOffsetY(),
mBackground.getScaledRadius());
mBackground.drawBackground(canvas);
}
@@ -622,17 +615,6 @@ public class FolderIcon extends FrameLayout implements FloatingIconViewCompanion
drawDot(canvas);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
releaseGlassBlur();
}
@Override
public void releaseGlassBlur() {
mGlassBlur.release();
}
public void drawDot(Canvas canvas) {
if (!mForceHideDot && ((mDotInfo != null && mDotInfo.hasDot()) || mDotScale > 0)) {
Rect iconBounds = mDotParams.iconBounds;
@@ -113,9 +113,6 @@ public class DoubleShadowBubbleTextView extends BubbleTextView {
}
int alpha = Color.alpha(getCurrentTextColor());
// LC-Note: this path skips BubbleTextView#onDraw, which draws the glass blur
drawGlassBlurIfNecessary(canvas);
// We enhance the shadow by drawing the shadow twice
getPaint().setShadowLayer(mShadowInfo.getAmbientShadowBlur(), 0, 0,
getTextShadowColor(mShadowInfo.getAmbientShadowColor(), alpha));
@@ -17,7 +17,6 @@
package com.android.launcher3.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Rect;
import android.view.LayoutInflater;
@@ -30,13 +29,10 @@ import androidx.annotation.UiThread;
import com.android.launcher3.R;
import com.android.launcher3.util.Executors;
import app.lawnchair.blur.GlassBlur;
/**
* Launcher AppWidgetHostView with support for rounded corners and a fallback View.
*/
public abstract class BaseLauncherAppWidgetHostView extends NavigableAppWidgetHostView implements LocalColorExtractor.Listener,
GlassBlur.Host {
public abstract class BaseLauncherAppWidgetHostView extends NavigableAppWidgetHostView implements LocalColorExtractor.Listener {
private static final ViewOutlineProvider VIEW_OUTLINE_PROVIDER = new ViewOutlineProvider() {
@Override
@@ -69,10 +65,6 @@ public abstract class BaseLauncherAppWidgetHostView extends NavigableAppWidgetHo
// Lawnchair: LocalColorExtractor
private final LocalColorExtractor mColorExtractor;
// LC-Note: live blur of the wallpaper behind the widget background
private final GlassBlur mGlassBlur = new GlassBlur(this, GlassBlur.Type.WIDGET);
private final Rect mGlassBlurBounds = new Rect();
public BaseLauncherAppWidgetHostView(Context context) {
super(context);
@@ -107,27 +99,6 @@ public abstract class BaseLauncherAppWidgetHostView extends NavigableAppWidgetHo
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mColorExtractor.setListener(null);
releaseGlassBlur();
}
@Override
public void releaseGlassBlur() {
mGlassBlur.release();
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (canDrawGlassBlur()) {
mGlassBlur.drawWidget(canvas, mGlassBlurBounds, mEnforcedCornerRadius);
}
super.dispatchDraw(canvas);
}
/**
* LC-Note: placeholders draw their content before the glass blur would punch through it.
*/
protected boolean canDrawGlassBlur() {
return true;
}
@Override
@@ -139,15 +110,6 @@ public abstract class BaseLauncherAppWidgetHostView extends NavigableAppWidgetHo
}
enforceRoundedCorners();
if (mGlassBlur.isEnabled()) {
View background = RoundedCornerEnforcement.findBackground(this);
if (background == null) {
mGlassBlurBounds.setEmpty();
} else {
RoundedCornerEnforcement.computeRoundedRectangle(this, background,
mGlassBlurBounds);
}
}
}
@UiThread
@@ -449,11 +449,6 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView
}
}
@Override
protected boolean canDrawGlassBlur() {
return false;
}
@Override
protected void onDraw(Canvas canvas) {
if (mPreviewBitmap != null