From 5a4da9804823ae461dd38e18909051b691678412 Mon Sep 17 00:00:00 2001 From: Anthonyy232 <60626873+Anthonyy232@users.noreply.github.com> Date: Thu, 30 Jul 2026 00:19:48 -0700 Subject: [PATCH 1/5] fix: harden wallpaper engines for v4.0.2 --- CHANGELOG.md | 17 ++++ app/build.gradle.kts | 4 +- .../util/WallpaperUtilInstrumentedTest.kt | 89 +++++++++++++++++++ .../presentation/screens/home/HomeScreen.kt | 2 +- .../screens/home/HomeViewModel.kt | 11 ++- .../screens/wallpaper/WallpaperScreen.kt | 11 ++- .../paperize/service/alarm/BootReceiver.kt | 5 ++ .../PaperizeLiveWallpaperService.kt | 34 ++++++- .../service/livewallpaper/gl/GLPicture.kt | 7 +- .../livewallpaper/renderer/GLGeometry.kt | 19 ++++ .../livewallpaper/renderer/GLShaders.kt | 8 +- .../renderer/PaperizeWallpaperRenderer.kt | 74 +++++++++++---- .../livewallpaper/renderer/GLGeometryTest.kt | 33 +++++++ 13 files changed, 288 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bafb0748..45936048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## v4.0.2 + +- Reloaded the current live wallpaper at native resolution after fold, unfold, + surface-size, and scaling changes without advancing the wallpaper queue. +- Made adaptive brightness update the wallpaper already on screen and removed + the brightness dip that could occur halfway through live crossfades. +- Kept live vignette shading continuous across large images split into multiple + GPU texture tiles. +- Prevented a delayed effect-slider save from overwriting a switch or other + setting changed immediately afterward. +- Added the missing daily album refresh to live schedules and hardened boot + recovery so valid jobs are restored without duplicates and stale jobs are removed. +- Expanded device coverage for every static effect and verified synchronized, + independent, manual, live, and reboot scheduling paths. + +**Full Changelog**: https://github.com/Anthonyy232/Paperize/compare/v4.0.1...v4.0.2 + ## v4.0.1 - Restored native static FILL scrolling so wide wallpapers move between their real diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 12eb7c3b..43d095db 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,8 +32,8 @@ android { applicationId = "com.anthonyla.paperize" minSdk = 31 targetSdk = 36 - versionCode = 52 - versionName = "4.0.1" + versionCode = 53 + versionName = "4.0.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/androidTest/java/com/anthonyla/paperize/core/util/WallpaperUtilInstrumentedTest.kt b/app/src/androidTest/java/com/anthonyla/paperize/core/util/WallpaperUtilInstrumentedTest.kt index bda67919..3060c06f 100644 --- a/app/src/androidTest/java/com/anthonyla/paperize/core/util/WallpaperUtilInstrumentedTest.kt +++ b/app/src/androidTest/java/com/anthonyla/paperize/core/util/WallpaperUtilInstrumentedTest.kt @@ -13,6 +13,7 @@ import java.io.File import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull +import org.junit.Assert.assertSame import org.junit.Assert.assertTrue import org.junit.Assume.assumeTrue import org.junit.Test @@ -106,6 +107,94 @@ class WallpaperUtilInstrumentedTest { assertEquals(expected?.second, actual.height) } + @Test + fun disabledStaticEffectsLeaveBitmapUntouched() { + val source = mutableBitmap(32, 32, Color.rgb(240, 80, 20)) + + val result = processBitmap( + source = source, + enableDarken = false, + darkenPercent = 100, + enableBlur = false, + blurPercent = 100, + enableVignette = false, + vignettePercent = 100, + enableGrayscale = false, + grayscalePercent = 100 + ) + + assertSame(source, result) + assertEquals(Color.rgb(240, 80, 20), result.getPixel(16, 16)) + result.recycle() + } + + @Test + fun staticDarkenAndGrayscaleEffectsChangePixels() { + val white = mutableBitmap(32, 32, Color.WHITE) + val darkened = processBitmap( + source = white, + enableDarken = true, + darkenPercent = 100 + ) + assertTrue(Color.red(darkened.getPixel(16, 16)) <= 2) + if (darkened !== white) white.recycle() + darkened.recycle() + + val red = mutableBitmap(32, 32, Color.RED) + val grayscale = processBitmap( + source = red, + enableGrayscale = true, + grayscalePercent = 100 + ) + val grayPixel = grayscale.getPixel(16, 16) + assertTrue(kotlin.math.abs(Color.red(grayPixel) - Color.green(grayPixel)) <= 2) + assertTrue(kotlin.math.abs(Color.green(grayPixel) - Color.blue(grayPixel)) <= 2) + if (grayscale !== red) red.recycle() + grayscale.recycle() + } + + @Test + fun staticVignetteDarkensEdgesMoreThanCenter() { + val source = mutableBitmap(96, 96, Color.WHITE) + val result = processBitmap( + source = source, + enableVignette = true, + vignettePercent = 75 + ) + + val center = Color.red(result.getPixel(48, 48)) + val corner = Color.red(result.getPixel(0, 0)) + assertTrue("Expected vignette corner ($corner) below center ($center)", corner < center) + if (result !== source) source.recycle() + result.recycle() + } + + @Test + fun staticBlurSoftensSharpBoundary() { + val source = Bitmap.createBitmap(96, 96, Bitmap.Config.ARGB_8888) + for (y in 0 until source.height) { + for (x in 0 until source.width) { + source.setPixel(x, y, if (x < 48) Color.BLACK else Color.WHITE) + } + } + + val result = processBitmap( + source = source, + enableBlur = true, + blurPercent = 60 + ) + + val boundary = Color.red(result.getPixel(48, 48)) + assertTrue("Expected blurred boundary, got $boundary", boundary in 2..253) + if (result !== source) source.recycle() + result.recycle() + } + + private fun mutableBitmap(width: Int, height: Int, color: Int): Bitmap = + Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply { + eraseColor(color) + } + private fun createJpeg(width: Int, height: Int): File { val file = File.createTempFile("paperize-test-", ".jpg", context.cacheDir) files += file diff --git a/app/src/main/java/com/anthonyla/paperize/presentation/screens/home/HomeScreen.kt b/app/src/main/java/com/anthonyla/paperize/presentation/screens/home/HomeScreen.kt index 4d05c268..d299eeb6 100644 --- a/app/src/main/java/com/anthonyla/paperize/presentation/screens/home/HomeScreen.kt +++ b/app/src/main/java/com/anthonyla/paperize/presentation/screens/home/HomeScreen.kt @@ -116,7 +116,7 @@ fun HomeScreen( } else { WallpaperScreen( albums = albums, - scheduleSettings = scheduleSettings, + persistedScheduleSettings = scheduleSettings, appSettings = appSettings, wallpaperMode = wallpaperMode!!, onToggleChanger = { viewModel.toggleWallpaperChanger(it, onlyIfNotScheduled = true) }, diff --git a/app/src/main/java/com/anthonyla/paperize/presentation/screens/home/HomeViewModel.kt b/app/src/main/java/com/anthonyla/paperize/presentation/screens/home/HomeViewModel.kt index 6d81003a..229a6447 100644 --- a/app/src/main/java/com/anthonyla/paperize/presentation/screens/home/HomeViewModel.kt +++ b/app/src/main/java/com/anthonyla/paperize/presentation/screens/home/HomeViewModel.kt @@ -366,6 +366,12 @@ class HomeViewModel @Inject constructor( } fun updateScheduleSettings(settings: ScheduleSettings) { + pendingSettingsJob?.cancel() + pendingSettingsJob = null + applyScheduleSettings(settings) + } + + private fun applyScheduleSettings(settings: ScheduleSettings) { viewModelScope.launch { // Check if settings have changed before validation val currentSettings = settingsRepository.getScheduleSettings() @@ -456,7 +462,8 @@ class HomeViewModel @Inject constructor( pendingSettingsJob?.cancel() pendingSettingsJob = viewModelScope.launch { delay(Constants.SETTINGS_DEBOUNCE_MS) - updateScheduleSettings(settings) + pendingSettingsJob = null + applyScheduleSettings(settings) } } @@ -512,8 +519,10 @@ class HomeViewModel @Inject constructor( ScreenType.LIVE, settings.liveIntervalMinutes ) + wallpaperScheduler.scheduleAlbumRefresh() } else { wallpaperScheduler.cancelWallpaperChange(ScreenType.LIVE) + wallpaperScheduler.cancelAlbumRefresh() } return } diff --git a/app/src/main/java/com/anthonyla/paperize/presentation/screens/wallpaper/WallpaperScreen.kt b/app/src/main/java/com/anthonyla/paperize/presentation/screens/wallpaper/WallpaperScreen.kt index 4a830dbc..964251f1 100644 --- a/app/src/main/java/com/anthonyla/paperize/presentation/screens/wallpaper/WallpaperScreen.kt +++ b/app/src/main/java/com/anthonyla/paperize/presentation/screens/wallpaper/WallpaperScreen.kt @@ -62,7 +62,7 @@ enum class AlbumSelectionContext { @Composable fun WallpaperScreen( albums: List, - scheduleSettings: ScheduleSettings, + persistedScheduleSettings: ScheduleSettings, appSettings: AppSettings, wallpaperMode: WallpaperMode, onToggleChanger: (Boolean) -> Unit, @@ -79,12 +79,21 @@ fun WallpaperScreen( var showAlbumSelectionSheet by rememberSaveable { mutableStateOf(false) } var albumSelectionContext by rememberSaveable { mutableStateOf(AlbumSelectionContext.BOTH) } var showEmptyAlbumWarning by rememberSaveable { mutableStateOf(false) } + var scheduleSettings by remember { mutableStateOf(persistedScheduleSettings) } + + // Keep an immediate local draft so a slider value waiting for the ViewModel debounce + // is included in a switch or other setting changed before that debounce expires. + LaunchedEffect(persistedScheduleSettings) { + scheduleSettings = persistedScheduleSettings + } fun updateSettingsDebounced(newSettings: ScheduleSettings) { + scheduleSettings = newSettings onUpdateScheduleSettingsDebounced(newSettings) } fun updateSettingsImmediate(newSettings: ScheduleSettings) { + scheduleSettings = newSettings onUpdateScheduleSettings(newSettings) } diff --git a/app/src/main/java/com/anthonyla/paperize/service/alarm/BootReceiver.kt b/app/src/main/java/com/anthonyla/paperize/service/alarm/BootReceiver.kt index db38a1f2..1644fa35 100644 --- a/app/src/main/java/com/anthonyla/paperize/service/alarm/BootReceiver.kt +++ b/app/src/main/java/com/anthonyla/paperize/service/alarm/BootReceiver.kt @@ -58,8 +58,11 @@ class BootReceiver : BroadcastReceiver() { ScreenType.LIVE, settings.liveIntervalMinutes ) + wallpaperScheduler.scheduleAlbumRefresh() Log.d(TAG, "Live wallpaper changes scheduled on boot") } else { + wallpaperScheduler.cancelWallpaperChange(ScreenType.LIVE) + wallpaperScheduler.cancelAlbumRefresh() Log.d(TAG, "Live mode but no album or interval, not scheduling") } } else { @@ -105,10 +108,12 @@ class BootReceiver : BroadcastReceiver() { Log.d(TAG, "Wallpaper changes rescheduled successfully") } else { + wallpaperScheduler.cancelAllWallpaperChanges() Log.d(TAG, "Wallpaper changer enabled but required albums not selected, not scheduling") } } } else { + wallpaperScheduler.cancelAllWallpaperChanges() Log.d(TAG, "Wallpaper changer disabled, not scheduling") } } catch (e: Exception) { diff --git a/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/PaperizeLiveWallpaperService.kt b/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/PaperizeLiveWallpaperService.kt index 1aa85367..b3603d13 100644 --- a/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/PaperizeLiveWallpaperService.kt +++ b/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/PaperizeLiveWallpaperService.kt @@ -11,6 +11,7 @@ import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import com.anthonyla.paperize.core.ScreenType +import com.anthonyla.paperize.core.ScalingType import com.anthonyla.paperize.domain.repository.SettingsRepository import com.anthonyla.paperize.domain.repository.WallpaperRepository import com.anthonyla.paperize.service.livewallpaper.gl.GLWallpaperService @@ -102,6 +103,8 @@ class PaperizeLiveWallpaperService : GLWallpaperService(), LifecycleOwner { private lateinit var renderController: PaperizeRenderController private val engineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private var currentAlbumId: String? = null + @Volatile private var currentWallpaper: Wallpaper? = null + private var observedScalingType: ScalingType? = null private var hasShownParallaxWarning = false private val gestureDetector = GestureDetector( @@ -214,6 +217,8 @@ class PaperizeLiveWallpaperService : GLWallpaperService(), LifecycleOwner { return@withContext EmptyImageLoader } + currentWallpaper = wallpaper + // Peek at queue to see if it needs refilling (not dequeuing, just checking) val nextInQueue = wallpaperRepository.getNextWallpaperInQueue(albumId, ScreenType.LIVE) if (nextInQueue == null) { @@ -285,9 +290,31 @@ class PaperizeLiveWallpaperService : GLWallpaperService(), LifecycleOwner { val effects = settings.liveEffects val scalingType = settings.liveScalingType + val albumChanged = albumId != currentAlbumId + if (albumChanged) { + currentWallpaper = null + } + renderer.updateEffects(effects) renderer.updateScalingType(scalingType) renderer.updateAdaptiveBrightness(settings.adaptiveBrightness) + + val scalingChanged = + observedScalingType != null && observedScalingType != scalingType + observedScalingType = scalingType + + if (scalingChanged && !albumChanged) { + currentWallpaper?.let { wallpaper -> + Log.d(TAG, "Live scaling changed; reloading current wallpaper without advancing") + renderer.queueWallpaper( + ContentUriImageLoader( + contentResolver, + wallpaper.uri.toUri(), + scalingType + ) + ) + } + } // Show Toast warning if parallax is enabled but device has offset issues if (effects.enableParallax && !hasShownParallaxWarning && GLCompatibility.shouldWarnAboutParallax()) { @@ -302,7 +329,7 @@ class PaperizeLiveWallpaperService : GLWallpaperService(), LifecycleOwner { } // Reload if album changed - if (albumId != currentAlbumId) { + if (albumChanged) { Log.d(TAG, "Album changed from $currentAlbumId to $albumId, reloading") currentAlbumId = albumId renderController.reloadCurrentArtwork() @@ -318,6 +345,11 @@ class PaperizeLiveWallpaperService : GLWallpaperService(), LifecycleOwner { override fun onVisibilityChanged(visible: Boolean) { renderController.visible = visible + if (visible) { + // Re-evaluate draw-time effects such as adaptive brightness after + // configuration changes while the wallpaper was hidden. + requestRender() + } super.onVisibilityChanged(visible) } diff --git a/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/gl/GLPicture.kt b/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/gl/GLPicture.kt index 0ce671ed..5cb4445d 100644 --- a/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/gl/GLPicture.kt +++ b/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/gl/GLPicture.kt @@ -20,7 +20,12 @@ import kotlin.math.min */ class GLPicture( bitmap: Bitmap, - val brightnessFactor: Float = 1.0f + /** + * Luminance of the unmodified source bitmap. The renderer derives the adaptive + * brightness multiplier at draw time so toggling the setting affects the image + * that is already on screen. + */ + val sourceBrightness: Float ) { companion object { diff --git a/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/renderer/GLGeometry.kt b/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/renderer/GLGeometry.kt index 50cdb536..060bc636 100644 --- a/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/renderer/GLGeometry.kt +++ b/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/renderer/GLGeometry.kt @@ -19,6 +19,25 @@ object GLGeometry { val horizontalOffset: Float ) + data class CrossfadeAlphas( + val current: Float, + val next: Float + ) + + /** + * Alpha values for source-over crossfading. + * + * The current layer must stay opaque while the next layer fades over it. Fading + * both layers produces a visible brightness dip at the midpoint. + */ + fun calculateCrossfadeAlphas( + progress: Float, + hasNextPicture: Boolean + ): CrossfadeAlphas { + val nextAlpha = if (hasNextPicture) progress.coerceIn(0f, 1f) else 0f + return CrossfadeAlphas(current = 1f, next = nextAlpha) + } + /** * Calculate live-wallpaper scale and launcher-scroll offset without OpenGL dependencies. */ diff --git a/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/renderer/GLShaders.kt b/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/renderer/GLShaders.kt index d0d5ceb4..10b5a9d1 100644 --- a/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/renderer/GLShaders.kt +++ b/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/renderer/GLShaders.kt @@ -15,10 +15,15 @@ object GLShaders { attribute vec4 a_position; attribute vec2 a_texCoord; varying vec2 v_texCoord; + varying vec2 v_imageCoord; void main() { gl_Position = u_mvpMatrix * a_position; v_texCoord = a_texCoord; + // a_position spans the complete picture even when its texture is split + // into tiles. Keep a global image coordinate so image-wide effects do + // not restart at every tile boundary. + v_imageCoord = vec2(a_position.x * 0.5 + 0.5, 0.5 - a_position.y * 0.5); } """ @@ -132,6 +137,7 @@ object GLShaders { uniform float u_grayscaleFactor; uniform float u_adaptiveBrightnessFactor; varying vec2 v_texCoord; + varying vec2 v_imageCoord; void main() { vec4 color = texture2D(u_texture, v_texCoord); @@ -142,7 +148,7 @@ object GLShaders { // 2. Apply vignette — matches CPU vignetteBitmap gradient stops: // [0% dark at center] → [10% dark at 70% of radius] → [80% dark at edge] // radius is normalized to 0.5 UV (image edge along shorter axis) - vec2 vignetteCenter = v_texCoord - 0.5; + vec2 vignetteCenter = v_imageCoord - 0.5; float dist = length(vignetteCenter); float t = clamp(dist / 0.5, 0.0, 1.0); // Inner segment: 0% → 10% over [0, 0.7] diff --git a/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/renderer/PaperizeWallpaperRenderer.kt b/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/renderer/PaperizeWallpaperRenderer.kt index b9cf1789..69d23b2f 100644 --- a/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/renderer/PaperizeWallpaperRenderer.kt +++ b/app/src/main/java/com/anthonyla/paperize/service/livewallpaper/renderer/PaperizeWallpaperRenderer.kt @@ -120,6 +120,10 @@ class PaperizeWallpaperRenderer( // Adaptive brightness (from ScheduleSettings) @Volatile private var adaptiveBrightnessEnabled = false + // Retain the current source so a fold/unfold or other surface-size change can + // decode the same wallpaper again at the new native resolution. + @Volatile private var currentImageLoader: ImageLoader? = null + // Matrices private val mvpMatrix = FloatArray(16) private val projectionMatrix = FloatArray(16) @@ -156,6 +160,10 @@ class PaperizeWallpaperRenderer( override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) { Log.d(TAG, "onSurfaceChanged: ${width}x${height}") + val sizeChanged = + surfaceWidth > 0 && + surfaceHeight > 0 && + (surfaceWidth != width || surfaceHeight != height) surfaceWidth = width surfaceHeight = height @@ -163,6 +171,13 @@ class PaperizeWallpaperRenderer( // Recreate framebuffers for blur at new resolution createBlurFramebuffers(width, height) + + if (sizeChanged) { + currentImageLoader?.let { loader -> + Log.d(TAG, "Surface size changed; reloading current wallpaper at ${width}x${height}") + queueWallpaper(loader, skipCrossfade = true) + } + } } override fun onDrawFrame(gl: GL10) { @@ -190,9 +205,13 @@ class PaperizeWallpaperRenderer( } // Draw current picture + val crossfadeAlphas = GLGeometry.calculateCrossfadeAlphas( + progress = crossfadeProgress, + hasNextPicture = next != null + ) + current?.let { picture -> - val alpha = if (next != null) 1.0f - crossfadeProgress else 1.0f - drawPictureWithEffects(picture, alpha, blurRadius) + drawPictureWithEffects(picture, crossfadeAlphas.current, blurRadius) } // Draw next picture (if crossfading) @@ -203,7 +222,7 @@ class PaperizeWallpaperRenderer( } // Draw next picture with its own alpha - drawPictureWithEffects(picture, crossfadeProgress, blurRadius) + drawPictureWithEffects(picture, crossfadeAlphas.next, blurRadius) // Update crossfade progress using time-based calculation // This ensures consistent animation duration regardless of refresh rate (60Hz, 90Hz, 120Hz, etc.) @@ -297,7 +316,7 @@ class PaperizeWallpaperRenderer( uGrayscaleFactorHandle, if (currentEffects.enableGrayscale) currentEffects.grayscalePercentage / Constants.PERCENTAGE_DIVISOR else 0f ) - GLES20.glUniform1f(uAdaptiveBrightnessFactorHandle, picture.brightnessFactor) + GLES20.glUniform1f(uAdaptiveBrightnessFactorHandle, adaptiveBrightnessFactor(picture)) // Bind the fully blurred texture as input GLES20.glActiveTexture(GLES20.GL_TEXTURE0) @@ -328,7 +347,7 @@ class PaperizeWallpaperRenderer( uGrayscaleFactorHandle, if (currentEffects.enableGrayscale) currentEffects.grayscalePercentage / Constants.PERCENTAGE_DIVISOR else 0f ) - GLES20.glUniform1f(uAdaptiveBrightnessFactorHandle, picture.brightnessFactor) + GLES20.glUniform1f(uAdaptiveBrightnessFactorHandle, adaptiveBrightnessFactor(picture)) picture.draw(effectsProgram, aPositionHandle, aTexCoordHandle, mvpMatrix, uMvpMatrixHandle) } @@ -558,17 +577,15 @@ class PaperizeWallpaperRenderer( return@launch } - // Calculate adaptive brightness if enabled - val brightnessFactor = if (adaptiveBrightnessEnabled) { - val brightness = com.anthonyla.paperize.core.util.calculateBitmapBrightness(bitmap) - com.anthonyla.paperize.core.util.getAdaptiveBrightnessMultiplier(context, brightness) - } else { - 1.0f - } + // Always retain source luminance. Whether adaptive brightness is enabled + // is evaluated at draw time so settings changes are immediate and do not + // advance the wallpaper queue. + val sourceBrightness = + com.anthonyla.paperize.core.util.calculateBitmapBrightness(bitmap) // Upload to GPU on GL thread callbacks.queueEventOnGlThread { - uploadBitmap(bitmap, brightnessFactor, skipCrossfade) + uploadBitmap(bitmap, sourceBrightness, imageLoader, skipCrossfade) } } else { Log.w(TAG, "Failed to load wallpaper (null bitmap)") @@ -614,10 +631,16 @@ class PaperizeWallpaperRenderer( * Must be called on GL thread. * * @param bitmap The bitmap to upload - * @param brightnessFactor The adaptive brightness multiplier for this bitmap + * @param sourceBrightness Luminance of the unmodified source bitmap + * @param imageLoader Source used to reproduce this bitmap after a surface resize * @param skipCrossfade If true, instantly replace current wallpaper without animation */ - private fun uploadBitmap(bitmap: Bitmap, brightnessFactor: Float, skipCrossfade: Boolean = false) { + private fun uploadBitmap( + bitmap: Bitmap, + sourceBrightness: Float, + imageLoader: ImageLoader, + skipCrossfade: Boolean = false + ) { // Validate bitmap before processing if (bitmap.isRecycled) { Log.e(TAG, "Cannot upload recycled bitmap") @@ -630,7 +653,8 @@ class PaperizeWallpaperRenderer( } try { - val picture = GLPicture(bitmap, brightnessFactor) + val picture = GLPicture(bitmap, sourceBrightness) + currentImageLoader = imageLoader // Recycle bitmap (no longer needed after GPU upload) bitmap.recycle() @@ -701,10 +725,23 @@ class PaperizeWallpaperRenderer( * Can be called from any thread. */ fun updateAdaptiveBrightness(enabled: Boolean) { - adaptiveBrightnessEnabled = enabled - Log.d(TAG, "Adaptive brightness updated: $enabled") + if (adaptiveBrightnessEnabled != enabled) { + adaptiveBrightnessEnabled = enabled + Log.d(TAG, "Adaptive brightness updated: $enabled") + callbacks.requestRender() + } } + private fun adaptiveBrightnessFactor(picture: GLPicture): Float = + if (adaptiveBrightnessEnabled) { + com.anthonyla.paperize.core.util.getAdaptiveBrightnessMultiplier( + context, + picture.sourceBrightness + ) + } else { + 1f + } + /** * Update scaling type. * Can be called from any thread. @@ -730,6 +767,7 @@ class PaperizeWallpaperRenderer( nextPicture?.recycle() nextPicture = null + currentImageLoader = null GLUtil.deleteProgram(simpleProgram) GLUtil.deleteProgram(blurHorizontalProgram) diff --git a/app/src/test/java/com/anthonyla/paperize/service/livewallpaper/renderer/GLGeometryTest.kt b/app/src/test/java/com/anthonyla/paperize/service/livewallpaper/renderer/GLGeometryTest.kt index 54795a9f..56f8420c 100644 --- a/app/src/test/java/com/anthonyla/paperize/service/livewallpaper/renderer/GLGeometryTest.kt +++ b/app/src/test/java/com/anthonyla/paperize/service/livewallpaper/renderer/GLGeometryTest.kt @@ -62,6 +62,39 @@ class GLGeometryTest { assertEquals(75f, result.horizontalOffset, 0.001f) } + @Test + fun `crossfade keeps current opaque while next fades in`() { + val start = GLGeometry.calculateCrossfadeAlphas(0f, hasNextPicture = true) + val midpoint = GLGeometry.calculateCrossfadeAlphas(0.5f, hasNextPicture = true) + val end = GLGeometry.calculateCrossfadeAlphas(1f, hasNextPicture = true) + + assertEquals(1f, start.current, 0.001f) + assertEquals(0f, start.next, 0.001f) + assertEquals(1f, midpoint.current, 0.001f) + assertEquals(0.5f, midpoint.next, 0.001f) + assertEquals(1f, end.current, 0.001f) + assertEquals(1f, end.next, 0.001f) + } + + @Test + fun `crossfade clamps progress and disables next alpha when absent`() { + assertEquals( + 0f, + GLGeometry.calculateCrossfadeAlphas(-1f, hasNextPicture = true).next, + 0.001f + ) + assertEquals( + 1f, + GLGeometry.calculateCrossfadeAlphas(2f, hasNextPicture = true).next, + 0.001f + ) + assertEquals( + 0f, + GLGeometry.calculateCrossfadeAlphas(0.75f, hasNextPicture = false).next, + 0.001f + ) + } + private fun transform( scalingType: ScalingType, offset: Float, From 8e8c1d681761484644f97901a28a15eb26aa4b94 Mon Sep 17 00:00:00 2001 From: Anthonyy232 <60626873+Anthonyy232@users.noreply.github.com> Date: Thu, 30 Jul 2026 00:26:00 -0700 Subject: [PATCH 2/5] fix: restore launcher wallpaper shortcut --- CHANGELOG.md | 2 ++ app/src/main/AndroidManifest.xml | 10 ++++++++ .../shortcut/WallpaperShortcutActivity.kt | 23 +++++++++++++++++ .../wallpaper/WallpaperChangeService.kt | 25 +++++++++++++++++-- app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/shortcuts.xml | 14 +++++++++++ 6 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/anthonyla/paperize/service/shortcut/WallpaperShortcutActivity.kt create mode 100644 app/src/main/res/xml/shortcuts.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 45936048..3af650dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ GPU texture tiles. - Prevented a delayed effect-slider save from overwriting a switch or other setting changed immediately afterward. +- Restored the launcher app shortcut for gesture apps and other launchers, with + automatic routing to the configured static or live wallpaper engine. - Added the missing daily album refresh to live schedules and hardened boot recovery so valid jobs are restored without duplicates and stale jobs are removed. - Expanded device coverage for every static effect and verified synchronized, diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 61f52eee..98e308c6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,8 +49,18 @@ + + + handleChangeWallpaper(screenType, startId) + ACTION_CHANGE_WALLPAPER_AUTO -> + handleChangeWallpaper(screenType, startId, respectWallpaperMode = true) ACTION_REAPPLY_EFFECTS -> handleReapplyEffects(screenType, startId) else -> { Log.w(TAG, "Unknown action: ${intent?.action}") @@ -80,11 +83,27 @@ class WallpaperChangeService : Service() { return START_NOT_STICKY } - private fun handleChangeWallpaper(screenType: ScreenType, startId: Int) { + private fun handleChangeWallpaper( + screenType: ScreenType, + startId: Int, + respectWallpaperMode: Boolean = false + ) { serviceScope.launch { wallpaperChangeLock.mutex.withLock { try { - changeWallpaper(screenType, settingsRepository.getScheduleSettings()) + val effectiveScreenType = + if ( + respectWallpaperMode && + settingsRepository.getWallpaperMode() == WallpaperMode.LIVE + ) { + ScreenType.LIVE + } else { + screenType + } + changeWallpaper( + effectiveScreenType, + settingsRepository.getScheduleSettings() + ) } catch (e: Exception) { Log.e(TAG, "Error changing wallpaper", e) showErrorNotification( @@ -401,6 +420,8 @@ class WallpaperChangeService : Service() { companion object { private const val TAG = "WallpaperChangeService" const val ACTION_CHANGE_WALLPAPER = Constants.ACTION_CHANGE_WALLPAPER + const val ACTION_CHANGE_WALLPAPER_AUTO = + "com.anthonyla.paperize.ACTION_CHANGE_WALLPAPER_AUTO" const val ACTION_REAPPLY_EFFECTS = Constants.ACTION_REAPPLY_EFFECTS const val EXTRA_SCREEN_TYPE = Constants.EXTRA_SCREEN_TYPE private const val ERROR_NOTIFICATION_ID = Constants.NOTIFICATION_ID + 1 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9b7b3b1f..11da5d0b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -207,5 +207,6 @@ Optimizing (May delay changes) Reliability Change wallpaper now + Change to the next configured wallpaper diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml new file mode 100644 index 00000000..ef9a03d7 --- /dev/null +++ b/app/src/main/res/xml/shortcuts.xml @@ -0,0 +1,14 @@ + + + + + + From dd375a861002fd64c7a4ac96b0df7474931e8142 Mon Sep 17 00:00:00 2001 From: Anthonyy232 <60626873+Anthonyy232@users.noreply.github.com> Date: Thu, 30 Jul 2026 00:30:17 -0700 Subject: [PATCH 3/5] fix: keep launcher shortcut entry point private --- app/src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 98e308c6..e810b2ff 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,8 +57,9 @@ Date: Thu, 30 Jul 2026 00:38:38 -0700 Subject: [PATCH 4/5] chore: remove unused legacy dependencies --- CHANGELOG.md | 2 ++ app/build.gradle.kts | 2 -- app/proguard-rules.pro | 4 +--- gradle/libs.versions.toml | 2 -- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af650dc..6c92a423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ automatic routing to the configured static or live wallpaper engine. - Added the missing daily album refresh to live schedules and hardened boot recovery so valid jobs are restored without duplicates and stale jobs are removed. +- Removed two unused legacy serialization and document-file dependencies from + the release package. - Expanded device coverage for every static effect and verified synchronized, independent, manual, live, and reboot scheduling paths. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 43d095db..dc28b881 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -105,7 +105,6 @@ dependencies { implementation(libs.androidx.animation) implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.lifecycle.runtime.compose) - implementation(libs.gson) implementation(libs.androidx.documentfile) implementation(libs.androidx.exifinterface) implementation(libs.zoomable) @@ -133,7 +132,6 @@ dependencies { implementation(libs.androidx.room.runtime) ksp(libs.androidx.room.compiler) implementation(libs.androidx.room.ktx) - implementation(libs.dfc) implementation (libs.kotlinx.serialization.json) implementation(libs.toolbar.compose) } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index d54e2b95..72e7fa2c 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -20,8 +20,6 @@ # hide the original source file name. #-renamesourcefileattribute SourceFile -keepattributes Signature --keep class com.google.gson.reflect.TypeToken { *; } --keep class * extends com.google.gson.reflect.TypeToken -keep class kotlin.coroutines.Continuation -keep class androidx.datastore.*.** {*;} @@ -61,4 +59,4 @@ # Keep Room database -keep class * extends androidx.room.RoomDatabase -keep @androidx.room.Entity class * --dontwarn androidx.room.paging.** \ No newline at end of file +-dontwarn androidx.room.paging.** diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5fc5caf5..da1fcef8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,9 +47,7 @@ coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" } coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" } -dfc = { module = "com.lazygeniouz:dfc", version = "1.5" } google-material = { module = "com.google.android.material:material", version.ref = "material" } -gson = { module = "com.google.code.gson:gson", version = "2.13.2" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } junit = { module = "junit:junit", version = "4.13.2" } From 9ae70a3576d337ee5fb8a1443aa1f40338654f85 Mon Sep 17 00:00:00 2001 From: Anthonyy232 <60626873+Anthonyy232@users.noreply.github.com> Date: Thu, 30 Jul 2026 00:42:44 -0700 Subject: [PATCH 5/5] ci: validate final audited tree