Skip to content

Update API models to match latest ImmichFrame server, fix retry logic#56

Open
dave-palt wants to merge 4 commits into
immichFrame:mainfrom
dave-palt:fix/update-api-models-and-retry-logic
Open

Update API models to match latest ImmichFrame server, fix retry logic#56
dave-palt wants to merge 4 commits into
immichFrame:mainfrom
dave-palt:fix/update-api-models-and-retry-logic

Conversation

@dave-palt
Copy link
Copy Markdown

@dave-palt dave-palt commented May 27, 2026

Fixes #55

Changes

Data model updates (Helpers.kt)

  • Sync ServerSettings with latest ImmichFrame server ClientSettingsDto:
    • Removed margin and unattendedMode (no longer in server response)
    • Added clockDateFormat, showProgressBar, showAlbumName, weatherIconUrl, imagePan
    • Made 8 fields nullable (clockFormat, photoDateFormat, imageLocationFormat, secondaryColor, style, layout, language, baseFontSize)
    • All fields have Kotlin defaults for safe Gson deserialization
  • Made ImageResponse fields nullable
  • Made Weather fields nullable with defaults
  • Added api/Config/Version endpoint to ApiService

Fix retry logic (fixes #55)

  • getServerSettings() in both MainActivity and ScreenSaverService:
    • 4xx HTTP errors (404, 401, 403) now fail immediately instead of retrying 36 times
    • 404 shows: "Make sure the URL points to an ImmichFrame server, not the Immich server directly"
    • 401/403 shows: "Check your Authorization Secret in settings"
    • Only 5xx and network errors continue to retry
    • Toast message changed from "Retrying..." to "Connecting to server..."

Null safety

  • Updated all downstream code in MainActivity, ScreenSaverService, and WidgetProvider to handle nullable fields from the updated models

Summary by CodeRabbit

  • New Features

    • Added a version endpoint for system information retrieval.
  • Bug Fixes

    • More robust handling of missing image and metadata fields to prevent display errors.
    • Safer merging and display of photo metadata in multi-image/portrait modes.
    • Improved settings-load retry logic with clearer, status-specific user messages.
    • Widget/image decoding now guarded against missing data; UI update stability improved.
    • Non-webview settings error toasts now show longer duration.

Review Change Stack

… for issue immichFrame#55

- Sync ServerSettings with latest ClientSettingsDto: remove margin and
  unattendedMode, add clockDateFormat, showProgressBar, showAlbumName,
  weatherIconUrl, imagePan fields; make appropriate fields nullable
- Make ImageResponse and Weather fields nullable with Kotlin defaults
- Add api/Config/Version endpoint to ApiService
- Fix getServerSettings() retry logic: 4xx errors (404, 401, 403) now
  fail immediately with descriptive messages instead of infinite retries
- Add null safety to all downstream code in MainActivity, ScreenSaverService,
  and WidgetProvider
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

Warning

Review limit reached

@dave-palt, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 47 minutes and 21 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 24c9a182-7ce7-491a-ac76-9555545c8b8e

📥 Commits

Reviewing files that changed from the base of the PR and between 1d0b8ee and ee52b71.

📒 Files selected for processing (2)
  • app/src/main/java/com/immichframe/immichframe/MainActivity.kt
  • app/src/main/java/com/immichframe/immichframe/ScreenSaverService.kt
📝 Walkthrough

Walkthrough

Refactors API models to be nullable/default-tolerant; makes image decoding and portrait-merge flows null-safe across MainActivity, ScreenSaverService, and WidgetProvider; updates photo/date/weather formatting to handle absent server settings; and refactors getServerSettings() to use HTTP-status-derived retryability with centralized failure handling.

Changes

API Resilience and Null-Safe UI Rendering

Layer / File(s) Summary
Data model contracts for nullable fields and versioning
app/src/main/java/com/immichframe/immichframe/Helpers.kt
ImageResponse, ServerSettings, and Weather data classes now treat most String fields as nullable with defaults; numeric/boolean fields in ServerSettings and Weather gain defaults; ApiService.getVersion() added (GET api/Config/Version).
UI image rendering with null-safe base64 validation
app/src/main/java/com/immichframe/immichframe/MainActivity.kt, app/src/main/java/com/immichframe/immichframe/ScreenSaverService.kt, app/src/main/java/com/immichframe/immichframe/WidgetProvider.kt
Image payload fields are read into locals and decoding/processing is executed only when base64/thumbHash fields are present; portrait-cache decode/merge guarded by presence checks; widget bitmap decode moved behind null-check and dispatched to IO.
Metadata composition and date/time formatting with nullable values
app/src/main/java/com/immichframe/immichframe/MainActivity.kt, app/src/main/java/com/immichframe/immichframe/ScreenSaverService.kt
Merged-photo date/location use orEmpty() and clear portrait cache after merge; photoDateFormat and clockFormat are checked for null before formatting; weather display composes from nullable fields with fallbacks (e.g., location → "Unknown").
Server settings error handling with status-based retry and improved hints
app/src/main/java/com/immichframe/immichframe/MainActivity.kt, app/src/main/java/com/immichframe/immichframe/ScreenSaverService.kt
getServerSettings() computes retryability from HTTP status codes (retryable when code !in 400..499), treats empty successful bodies as retryable failures, centralizes failure handling via handleFailure(t, retryable), adds specific hints for 404 and 401/403, and changes non-webview failure toast to LENGTH_LONG.

🎯 4 (Complex) | ⏱️ ~45 minutes

🐰 The frame now shows smarter, kinder ways,
With nullable fields and null-safe plays,
More graceful when data goes missing or late,
And retry hints help navigate fate,
The rabbits hop forward—more stable, more great!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly describes the two main changes: updating API models to match the latest server version and fixing retry logic for server settings requests.
Linked Issues check ✅ Passed The PR comprehensively addresses all coding requirements from issue #55: implements immediate failure for 4xx errors, provides actionable error messages for 404/401/403, retains retries for 5xx/network errors, and improves user-facing messaging.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the PR objectives: data model updates for server API alignment, retry logic fixes for issue #55, and null-safety updates in downstream code to support the model changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
app/src/main/java/com/immichframe/immichframe/ScreenSaverService.kt (1)

188-190: ⚡ Quick win

Potential race condition: capture portraitCache in a local variable.

Same issue as in MainActivity.kt - portraitCache is a mutable class field that can be modified from the Main thread while this coroutine runs on Dispatchers.IO.

♻️ Proposed fix
             val isPortrait = randomBitmap.height > randomBitmap.width
             if (isPortrait && serverSettings.layout == "splitview") {
-                if (portraitCache != null && portraitCache!!.randomImageBase64 != null) {
-                    var decodedPortraitImageBitmap =
-                        Helpers.decodeBitmapFromBytes(portraitCache!!.randomImageBase64!!)
+                val cachedBase64 = portraitCache?.randomImageBase64
+                if (cachedBase64 != null) {
+                    var decodedPortraitImageBitmap =
+                        Helpers.decodeBitmapFromBytes(cachedBase64)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/immichframe/immichframe/ScreenSaverService.kt` around
lines 188 - 190, The code reads the mutable field portraitCache inside a
coroutine on Dispatchers.IO which can race with main-thread updates; capture
portraitCache into a local val at the start of the coroutine (e.g. val
localPortrait = portraitCache) and then use localPortrait when checking
randomImageBase64 and calling Helpers.decodeBitmapFromBytes to ensure a
consistent snapshot; update references in ScreenSaverService where portraitCache
is accessed inside the IO coroutine so all null-checks and decode calls use the
local variable instead of the volatile field.
app/src/main/java/com/immichframe/immichframe/MainActivity.kt (1)

198-200: ⚡ Quick win

Potential race condition: capture portraitCache in a local variable.

portraitCache is a mutable class field that can be set to null by updateUI() on the Main thread (line 293) while this coroutine runs on Dispatchers.IO. The null check at line 198 doesn't guarantee the field remains non-null at line 200, risking an NPE.

♻️ Proposed fix to avoid TOCTOU race
             val isPortrait = randomBitmap.height > randomBitmap.width
             if (isPortrait && serverSettings.layout == "splitview") {
-                if (portraitCache != null && portraitCache!!.randomImageBase64 != null) {
-                    var decodedPortraitImageBitmap =
-                        Helpers.decodeBitmapFromBytes(portraitCache!!.randomImageBase64!!)
+                val cachedBase64 = portraitCache?.randomImageBase64
+                if (cachedBase64 != null) {
+                    var decodedPortraitImageBitmap =
+                        Helpers.decodeBitmapFromBytes(cachedBase64)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt` around lines
198 - 200, Capture the mutable field portraitCache into a local val at the start
of the coroutine before the null check to avoid a TOCTOU race (portraitCache can
be nulled by updateUI() on the main thread while this runs on Dispatchers.IO);
replace usages of portraitCache!! with the local variable (e.g., val
localPortrait = portraitCache; if (localPortrait?.randomImageBase64 != null) {
val decodedPortraitImageBitmap =
Helpers.decodeBitmapFromBytes(localPortrait.randomImageBase64) ... }) so you
never dereference the class field directly and eliminate the forced !! operator.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt`:
- Around line 198-200: Capture the mutable field portraitCache into a local val
at the start of the coroutine before the null check to avoid a TOCTOU race
(portraitCache can be nulled by updateUI() on the main thread while this runs on
Dispatchers.IO); replace usages of portraitCache!! with the local variable
(e.g., val localPortrait = portraitCache; if (localPortrait?.randomImageBase64
!= null) { val decodedPortraitImageBitmap =
Helpers.decodeBitmapFromBytes(localPortrait.randomImageBase64) ... }) so you
never dereference the class field directly and eliminate the forced !! operator.

In `@app/src/main/java/com/immichframe/immichframe/ScreenSaverService.kt`:
- Around line 188-190: The code reads the mutable field portraitCache inside a
coroutine on Dispatchers.IO which can race with main-thread updates; capture
portraitCache into a local val at the start of the coroutine (e.g. val
localPortrait = portraitCache) and then use localPortrait when checking
randomImageBase64 and calling Helpers.decodeBitmapFromBytes to ensure a
consistent snapshot; update references in ScreenSaverService where portraitCache
is accessed inside the IO coroutine so all null-checks and decode calls use the
local variable instead of the volatile field.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b4db99d-cea7-46ae-8cdd-b9ffac9259bc

📥 Commits

Reviewing files that changed from the base of the PR and between e40d7a1 and 6cdef22.

📒 Files selected for processing (4)
  • app/src/main/java/com/immichframe/immichframe/Helpers.kt
  • app/src/main/java/com/immichframe/immichframe/MainActivity.kt
  • app/src/main/java/com/immichframe/immichframe/ScreenSaverService.kt
  • app/src/main/java/com/immichframe/immichframe/WidgetProvider.kt

Capture mutable portraitCache field into a local val at the start of
the splitview branch to avoid racing with main-thread updates from
updateUI(). Eliminates forced !! operator dereferences.
randomBitmap
)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered removing this whole widget. Now that Immich includes one itself I am not sure it is necessary?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow the development of immich, so I did not know that there is a built-in widget.
Myself I am using this app without a widget nor immich with an old tablet, if there are people with a similar situation that also use the widget it might be worth keeping I guess?
In general, unless it become a big deal to maintain I don't see a particular reason to remove it, but it's up to you.

Copy link
Copy Markdown
Contributor

@3rob3 3rob3 May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow the development of immich, so I did not know that there is a built-in widget. Myself I am using this app without a widget nor immich with an old tablet, if there are people with a similar situation that also use the widget it might be worth keeping I guess? In general, unless it become a big deal to maintain I don't see a particular reason to remove it, but it's up to you.

What are you using it for then (out of curiosity)?
edit- nevermind, think you meant you aren't using either this widget or Immich's.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I am using immich in my phone and this app on the tablet, I don't really use widgets in general

val style: String? = null,
val baseFontSize: String? = null,
val showWeatherDescription: Boolean = false,
val weatherIconUrl: String? = null,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you add the 8 new/missing fields if they are not being used? I only included what we actually used.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not aware of the decision of including only the fields we used, parly because I did not think about this a something to consider.

I just updated the pr to discard the unused fields

@3rob3
Copy link
Copy Markdown
Contributor

3rob3 commented May 27, 2026

Nice job and nice improvements. This non-webview code has not gotten much attention so thanks! Just made a couple comments.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/src/main/java/com/immichframe/immichframe/MainActivity.kt (2)

279-289: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid rendering dangling | separators in merged metadata.

On Line 279 and Line 287, if only one side is present, the output becomes " | value" or "value | ".

💡 Proposed fix
-            val mergedPhotoDate =
-                if (cachedPhotoDate.isNotEmpty() || responsePhotoDate.isNotEmpty()) {
-                    "$cachedPhotoDate | $responsePhotoDate"
-                } else {
-                    ""
-                }
+            val mergedPhotoDate = listOf(cachedPhotoDate, responsePhotoDate)
+                .filter { it.isNotEmpty() }
+                .joinToString(" | ")

-            val mergedImageLocation =
-                if (cachedImageLocation.isNotEmpty() || responseImageLocation.isNotEmpty()) {
-                    "$cachedImageLocation | $responseImageLocation"
-                } else {
-                    ""
-                }
+            val mergedImageLocation = listOf(cachedImageLocation, responseImageLocation)
+                .filter { it.isNotEmpty() }
+                .joinToString(" | ")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt` around lines
279 - 289, The mergedPhotoDate and mergedImageLocation logic can produce
dangling " | " when one side is empty; change each merge to collect the two
parts (cachedPhotoDate and responsePhotoDate for mergedPhotoDate;
cachedImageLocation and responseImageLocation for mergedImageLocation), filter
out empty strings, and join the remaining parts with " | " (e.g., using a list +
filter + joinToString) so you only get "a", "a | b", or "" instead of " | b" or
"a | ".

503-505: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the class handler for retries so callbacks are canceled on destroy.

Line 503 creates a new Handler per retry, but onDestroy() only clears callbacks from the handler field. Retries can continue after activity teardown.

🛠️ Proposed fix
-                        Handler(Looper.getMainLooper()).postDelayed({
+                        handler.postDelayed({
                             attemptFetch()
                         }, retryDelayMillis)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt` around lines
503 - 505, The retry currently creates a fresh Handler per attempt
(Handler(Looper.getMainLooper()).postDelayed) which bypasses the activity-level
handler cancellation; change the retry to use the existing handler field (call
handler.postDelayed { attemptFetch() } with the same retryDelayMillis) and
ensure that handler is initialized on the main looper (e.g.,
Handler(Looper.getMainLooper())) so onDestroy()'s
handler.removeCallbacksAndMessages(null) will cancel pending retries; keep
references to the handler and attemptFetch() unchanged so callbacks are properly
removed in onDestroy().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt`:
- Around line 279-289: The mergedPhotoDate and mergedImageLocation logic can
produce dangling " | " when one side is empty; change each merge to collect the
two parts (cachedPhotoDate and responsePhotoDate for mergedPhotoDate;
cachedImageLocation and responseImageLocation for mergedImageLocation), filter
out empty strings, and join the remaining parts with " | " (e.g., using a list +
filter + joinToString) so you only get "a", "a | b", or "" instead of " | b" or
"a | ".
- Around line 503-505: The retry currently creates a fresh Handler per attempt
(Handler(Looper.getMainLooper()).postDelayed) which bypasses the activity-level
handler cancellation; change the retry to use the existing handler field (call
handler.postDelayed { attemptFetch() } with the same retryDelayMillis) and
ensure that handler is initialized on the main looper (e.g.,
Handler(Looper.getMainLooper())) so onDestroy()'s
handler.removeCallbacksAndMessages(null) will cancel pending retries; keep
references to the handler and attemptFetch() unchanged so callbacks are properly
removed in onDestroy().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be85a760-80d8-4bd9-b1d1-31366f140dc4

📥 Commits

Reviewing files that changed from the base of the PR and between 6cdef22 and 1d0b8ee.

📒 Files selected for processing (3)
  • app/src/main/java/com/immichframe/immichframe/Helpers.kt
  • app/src/main/java/com/immichframe/immichframe/MainActivity.kt
  • app/src/main/java/com/immichframe/immichframe/ScreenSaverService.kt
💤 Files with no reviewable changes (1)
  • app/src/main/java/com/immichframe/immichframe/Helpers.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/immichframe/immichframe/ScreenSaverService.kt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Outdated Endpoints? - Retrying to fetch server settings...

2 participants