Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@ class SettingsRepository(
const val KEY_AOD_WALLPAPER_VIGNETTE = "aod_wallpaper_vignette"
const val KEY_AOD_WALLPAPER_BLACK_THRESHOLD = "aod_wallpaper_black_threshold"
const val KEY_AOD_WALLPAPER_CUSTOM_IMAGE = "aod_wallpaper_custom_image"
const val KEY_AOD_WALLPAPER_CUSTOM_IMAGE_VERSION = "aod_wallpaper_custom_image_version"
const val KEY_AOD_WALLPAPER_COMPAT = "aod_wallpaper_compat_mode"
const val KEY_AOD_WALLPAPER_USE_ALBUM_ART = "aod_wallpaper_use_album_art"
const val KEY_AOD_WALLPAPER_DISABLE_ON_DND = "aod_wallpaper_disable_on_dnd"
const val KEY_AOD_WALLPAPER_KEEP_ON_MEDIA = "aod_wallpaper_keep_on_media"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ class AodWallpaperOverlayHandler(
private var isMediaListenerRegistered = false
private var crossfadeAnimator: ValueAnimator? = null
private var fadeInAnimator: ObjectAnimator? = null
private var fadeOutAnimator: ObjectAnimator? = null
private var pendingRemoval: Runnable? = null
private var pendingSwapBitmap: Bitmap? = null

private val powerManager by lazy { service.getSystemService(Context.POWER_SERVICE) as? PowerManager }
private val displayManager by lazy { service.getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager }
Expand Down Expand Up @@ -207,6 +210,36 @@ class AodWallpaperOverlayHandler(
}
}

private val removalFallbackRunnable = Runnable {
if (pendingRemoval != null) {
Log.w("AodWallpaperOverlay", "Fade-out did not finish, removing overlay directly")
runPendingRemoval()
}
}

private val fadeInFallbackRunnable = Runnable {
val container = overlayContainer ?: return@Runnable
if (isOverlayAdded && pendingRemoval == null && container.alpha < 1f) {
fadeInAnimator?.cancel()
fadeInAnimator = null
container.alpha = 1f
}
}

private val bitmapSwapFallbackRunnable = Runnable {
val bitmap = pendingSwapBitmap ?: return@Runnable
pendingSwapBitmap = null
val imageView = wallpaperImageView ?: return@Runnable
if (currentDisplayedBitmap !== bitmap) return@Runnable
crossfadeAnimator?.let {
crossfadeAnimator = null
it.removeAllListeners()
it.cancel()
}
imageView.setImageBitmap(bitmap)
imageView.alpha = prefs.getFloat(SettingsRepository.KEY_AOD_WALLPAPER_OPACITY, 0.3f)
}

private val timeoutRunnable: Runnable = Runnable {
if (isOverlayAdded && isScreenOff) {
val keepOnMedia = prefs.getBoolean(SettingsRepository.KEY_AOD_WALLPAPER_KEEP_ON_MEDIA, false)
Expand Down Expand Up @@ -733,6 +766,7 @@ class AodWallpaperOverlayHandler(
hideOverlay()
return
}
runPendingRemoval()
registerDisplayListener()
val opacity = prefs.getFloat(SettingsRepository.KEY_AOD_WALLPAPER_OPACITY, 0.3f)
val blurRadius = prefs.getFloat(SettingsRepository.KEY_AOD_WALLPAPER_BLUR, 0f)
Expand Down Expand Up @@ -813,18 +847,24 @@ class AodWallpaperOverlayHandler(

try {
val container = overlayContainer ?: return
val fadeIn = !compatMode()
container.visibility = View.VISIBLE
container.alpha = 0f
container.alpha = if (fadeIn) 0f else 1f
applyBurnInShift(animated = false)
windowManager?.addView(container, params)
isOverlayAdded = true

fadeInAnimator?.cancel()
fadeInAnimator =
ObjectAnimator.ofFloat(container, "alpha", 0f, 1f).apply {
duration = 2000
start()
}
fadeInAnimator = null
handler.removeCallbacks(fadeInFallbackRunnable)
if (fadeIn) {
fadeInAnimator =
ObjectAnimator.ofFloat(container, "alpha", 0f, 1f).apply {
duration = FADE_IN_DURATION_MS
start()
}
handler.postDelayed(fadeInFallbackRunnable, FADE_IN_DURATION_MS + ANIMATION_FALLBACK_SLACK_MS)
}

handler.removeCallbacks(burnInShiftRunnable)
handler.postDelayed(burnInShiftRunnable, BURN_IN_INTERVAL_MS)
Expand Down Expand Up @@ -1074,34 +1114,47 @@ class AodWallpaperOverlayHandler(

currentDisplayedBitmap = bitmap

if (!animated || imageView.drawable == null) {
handler.removeCallbacks(bitmapSwapFallbackRunnable)
pendingSwapBitmap = null
if (!animated || compatMode() || imageView.drawable == null) {
crossfadeAnimator?.cancel()
crossfadeAnimator = null
imageView.setImageBitmap(bitmap)
} else {
val targetOpacity = prefs.getFloat(SettingsRepository.KEY_AOD_WALLPAPER_OPACITY, 0.3f)
crossfadeAnimator?.cancel()
crossfadeAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
duration = 350
duration = CROSSFADE_HALF_DURATION_MS
addUpdateListener { animator ->
val fraction = animator.animatedValue as Float
imageView.alpha = targetOpacity * fraction
}
addListener(object : android.animation.AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: android.animation.Animator) {
imageView.setImageBitmap(bitmap)
ValueAnimator.ofFloat(0f, 1f).apply {
duration = 350
crossfadeAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = CROSSFADE_HALF_DURATION_MS
addUpdateListener { inAnim ->
val fraction = inAnim.animatedValue as Float
imageView.alpha = targetOpacity * fraction
}
addListener(object : android.animation.AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: android.animation.Animator) {
pendingSwapBitmap = null
handler.removeCallbacks(bitmapSwapFallbackRunnable)
}
})
start()
}
}
})
start()
}
pendingSwapBitmap = bitmap
handler.postDelayed(
bitmapSwapFallbackRunnable,
CROSSFADE_HALF_DURATION_MS * 2 + ANIMATION_FALLBACK_SLACK_MS,
)
}
}

Expand All @@ -1125,7 +1178,7 @@ class AodWallpaperOverlayHandler(
private fun extractCurrentWallpaper(): Bitmap? {
return try {
if (prefs.getBoolean(SettingsRepository.KEY_AOD_WALLPAPER_CUSTOM_IMAGE, false)) {
val file = File(service.filesDir, "custom_aod_wallpaper.png")
val file = File(service.filesDir, CUSTOM_IMAGE_FILE_NAME)
if (file.exists()) {
val customBmp = AppUtil.decodeSampledBitmapFromFile(file.absolutePath, reqWidth = 1440, reqHeight = 3200)
if (customBmp != null) return customBmp
Expand Down Expand Up @@ -1197,7 +1250,9 @@ class AodWallpaperOverlayHandler(
currentDisplayedBitmap = null
handler.removeCallbacks(burnInShiftRunnable)
handler.removeCallbacks(timeoutRunnable)
if (isOverlayAdded && overlayContainer != null) {
handler.removeCallbacks(fadeInFallbackRunnable)
handler.removeCallbacks(bitmapSwapFallbackRunnable)
if (isOverlayAdded && overlayContainer != null && pendingRemoval == null) {
val currentView = overlayContainer ?: return
val imageView = wallpaperImageView

Expand All @@ -1224,43 +1279,67 @@ class AodWallpaperOverlayHandler(
}
}

ObjectAnimator.ofFloat(currentView, "alpha", currentView.alpha, 0f).apply {
duration = 500
pendingRemoval = Runnable {
currentView.visibility = View.GONE
imageView?.translationX = 0f
imageView?.translationY = 0f
imageView?.scaleX = 1.0f
imageView?.scaleY = 1.0f
extendedInfoColumn?.translationX = 0f
extendedInfoColumn?.translationY = 0f
try {
windowManager?.removeView(currentView)
} catch (_: Exception) {
}
isOverlayAdded = false
overlayContainer = null
maskedContainer = null
wallpaperImageView = null
extendedLayer = null
extendedInfoColumn = null
mediaTitleText = null
mediaNoteIcon = null
appIconCachePackage = null
mediaSubtitleText = null
}

if (compatMode()) {
runPendingRemoval()
return
}
fadeOutAnimator = ObjectAnimator.ofFloat(currentView, "alpha", currentView.alpha, 0f).apply {
duration = FADE_OUT_DURATION_MS
addListener(object : android.animation.AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: android.animation.Animator) {
currentView.visibility = View.GONE
imageView?.translationX = 0f
imageView?.translationY = 0f
imageView?.scaleX = 1.0f
imageView?.scaleY = 1.0f
extendedInfoColumn?.translationX = 0f
extendedInfoColumn?.translationY = 0f
try {
windowManager?.removeView(currentView)
} catch (_: Exception) {
}
isOverlayAdded = false
overlayContainer = null
maskedContainer = null
wallpaperImageView = null
extendedLayer = null
extendedInfoColumn = null
mediaTitleText = null
mediaNoteIcon = null
appIconCachePackage = null
mediaSubtitleText = null
runPendingRemoval()
}
})
start()
}
handler.postDelayed(removalFallbackRunnable, FADE_OUT_DURATION_MS + ANIMATION_FALLBACK_SLACK_MS)
}
}

private fun compatMode(): Boolean = prefs.getBoolean(SettingsRepository.KEY_AOD_WALLPAPER_COMPAT, false)

private fun runPendingRemoval() {
val removal = pendingRemoval ?: return
pendingRemoval = null
handler.removeCallbacks(removalFallbackRunnable)
fadeOutAnimator?.let {
fadeOutAnimator = null
it.removeAllListeners()
it.cancel()
}
removal.run()
}

fun removeOverlay() {
unregisterWallpaperChangeListeners()
handler.removeCallbacks(burnInShiftRunnable)
handler.removeCallbacks(timeoutRunnable)
hideOverlay()
runPendingRemoval()
setLockScreenMediaSuppressed(false)
overlayContainer = null
maskedContainer = null
Expand All @@ -1274,5 +1353,10 @@ class AodWallpaperOverlayHandler(
const val LOCK_SCREEN_MEDIA_KEY = "media_controls_lock_screen"
const val KEY_SAVED_LOCK_SCREEN_MEDIA = "saved_lock_screen_media"
const val NO_SAVED_VALUE = -1
const val CUSTOM_IMAGE_FILE_NAME = "custom_aod_wallpaper.png"
const val FADE_IN_DURATION_MS = 2000L
const val FADE_OUT_DURATION_MS = 500L
const val CROSSFADE_HALF_DURATION_MS = 350L
const val ANIMATION_FALLBACK_SLACK_MS = 250L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -264,14 +264,16 @@ class ScreenOffAccessibilityService :
key == SettingsRepository.KEY_AOD_WALLPAPER_VIGNETTE ||
key == SettingsRepository.KEY_AOD_WALLPAPER_BLACK_THRESHOLD ||
key == SettingsRepository.KEY_AOD_WALLPAPER_CUSTOM_IMAGE ||
key == SettingsRepository.KEY_AOD_WALLPAPER_CUSTOM_IMAGE_VERSION ||
key == SettingsRepository.KEY_AOD_WALLPAPER_COMPAT ||
key == SettingsRepository.KEY_AOD_WALLPAPER_USE_ALBUM_ART ||
key == SettingsRepository.KEY_AOD_WALLPAPER_KEEP_ON_MEDIA ||
key == SettingsRepository.KEY_AOD_WALLPAPER_EXTENDED_MEDIA ||
key == SettingsRepository.KEY_AOD_WALLPAPER_EXTENDED_APP_ICON ||
key == SettingsRepository.KEY_AOD_WALLPAPER_EXTENDED_TEXT_SCALE ||
key == SettingsRepository.KEY_AOD_WALLPAPER_MEDIA_EXCLUDED_APPS
) {
if (key == SettingsRepository.KEY_AOD_WALLPAPER_CUSTOM_IMAGE) {
if (key == SettingsRepository.KEY_AOD_WALLPAPER_CUSTOM_IMAGE || key == SettingsRepository.KEY_AOD_WALLPAPER_CUSTOM_IMAGE_VERSION) {
aodWallpaperOverlayHandler.invalidateWallpaperCache()
}
aodWallpaperOverlayHandler.updateState()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2026 sameerasw.com
* License: MIT License
*
* Feature Module: UI Core Components
* File: AodCompatibilityInfoBottomSheet.kt
* Description: Info sheet explaining the AOD wallpaper compatibility mode.
*/

package com.sameerasw.essentials.ui.core.sheets

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.sameerasw.essentials.R
import com.sameerasw.essentials.utils.HapticUtil

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AodCompatibilityInfoBottomSheet(onDismissRequest: () -> Unit) {
val view = LocalView.current
EssentialsBottomSheet(onDismissRequest = onDismissRequest) {
Column(
modifier =
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp)
.padding(bottom = 32.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = stringResource(R.string.feat_aod_wallpaper_compat_title),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = stringResource(R.string.feat_aod_wallpaper_compat_info_why),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = stringResource(R.string.feat_aod_wallpaper_compat_info_what),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Button(
onClick = {
HapticUtil.performVirtualKeyHaptic(view)
onDismissRequest()
},
modifier = Modifier.fillMaxWidth(),
) {
Text(stringResource(R.string.action_got_it))
}
}
}
}
Loading
Loading