Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
## 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.
- 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.
- 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.

**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
Expand Down
6 changes: 2 additions & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
4 changes: 1 addition & 3 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -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.*.** {*;}

Expand Down Expand Up @@ -61,4 +59,4 @@
# Keep Room database
-keep class * extends androidx.room.RoomDatabase
-keep @androidx.room.Entity class *
-dontwarn androidx.room.paging.**
-dontwarn androidx.room.paging.**
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,19 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>

<activity
android:name=".service.shortcut.WallpaperShortcutActivity"
android:excludeFromRecents="true"
android:exported="false"
android:noHistory="true"
android:taskAffinity=""
android:theme="@android:style/Theme.NoDisplay" />

<service
android:name=".service.wallpaper.WallpaperChangeService"
android:enabled="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ fun HomeScreen(
} else {
WallpaperScreen(
albums = albums,
scheduleSettings = scheduleSettings,
persistedScheduleSettings = scheduleSettings,
appSettings = appSettings,
wallpaperMode = wallpaperMode!!,
onToggleChanger = { viewModel.toggleWallpaperChanger(it, onlyIfNotScheduled = true) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -456,7 +462,8 @@ class HomeViewModel @Inject constructor(
pendingSettingsJob?.cancel()
pendingSettingsJob = viewModelScope.launch {
delay(Constants.SETTINGS_DEBOUNCE_MS)
updateScheduleSettings(settings)
pendingSettingsJob = null
applyScheduleSettings(settings)
}
}

Expand Down Expand Up @@ -512,8 +519,10 @@ class HomeViewModel @Inject constructor(
ScreenType.LIVE,
settings.liveIntervalMinutes
)
wallpaperScheduler.scheduleAlbumRefresh()
} else {
wallpaperScheduler.cancelWallpaperChange(ScreenType.LIVE)
wallpaperScheduler.cancelAlbumRefresh()
}
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ enum class AlbumSelectionContext {
@Composable
fun WallpaperScreen(
albums: List<AlbumSummary>,
scheduleSettings: ScheduleSettings,
persistedScheduleSettings: ScheduleSettings,
appSettings: AppSettings,
wallpaperMode: WallpaperMode,
onToggleChanger: (Boolean) -> Unit,
Expand All @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()) {
Expand All @@ -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()
Expand All @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading