Skip to content

feat: screen turn-off#46

Open
dwolstenholme13 wants to merge 8 commits intoimmichFrame:mainfrom
dwolstenholme13:feature/ScreenTurnoff
Open

feat: screen turn-off#46
dwolstenholme13 wants to merge 8 commits intoimmichFrame:mainfrom
dwolstenholme13:feature/ScreenTurnoff

Conversation

@dwolstenholme13
Copy link
Copy Markdown

@dwolstenholme13 dwolstenholme13 commented Feb 28, 2026

This PR allows the screen to turn off when screen dimming is activated, rather than simply showing a black screen.

Currently, the app has a "Screen Dimming" feature which allows the user to specify a time range in which the screen will be dimmed, for instance at night when users would be asleep. However, this feature only displays a black screen and turns off the WebView, saving a small amount of power. On my 10.5-inch device, it brings power consumption down from 7W to 4W.

This PR changes the logic to turn off the flag keeping the screen on, and sets a very short (3s) time so the screen actually turns off. When the dimming function is activated, it turns off the WebView as before, but also saves the user's screen timeout setting value (in case they actually use this), changes it to 3s, and clears the screen keep-on flag, so a quick "Going to sleep" message is displayed and then the screen turns off. On my device, this brings power consumption down to less than 1W.

When the screen is to wake up again, it uses a wakeLock to turn the screen on, restores the original screen timeout value, sets the screen keep-on flag, and restores the WebView.

There was some help done with Android Studio's Gemini assistant, but all changes were done and reviewed by myself. Unfortunately, this has only been tested on my one device, a SSA ZN-DP1101 which runs Android 13.0. I attempted to ensure the code still runs properly on devices with older Android versions, but I do not have any such devices to test on.

Summary by CodeRabbit

  • New Features

    • Settings: added Screen Dimming category and a numeric Screen Timeout input to control automatic dimming and sleep behavior.
    • Widget configuration activity explicitly exported for proper widget setup.
  • Bug Fixes

    • More stable dim/undim flow with a short snooze to prevent immediate re-dimming; better wake/keyguard handling and restored system timeout on exit.
  • Chores

    • App version bumped, now requests permission to modify system display timeout, added device-admin support and build tool plugin.

… This works by changing the

screen timeout setting to a very short value, and clearing the FLAG_KEEP_SCREEN_ON flag, so the
power manager turns the screen off. When the screen is to turn on, a wake activity turns it back on,
and the keyguard lockscreen is dismissed, and the user's original screen timeout value is restored.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds persisted screen timeout, dim/undim and wake handling with snooze logic (MainActivity + Settings), new preferences and UI tweaks (screenTimeout, keepScreenOn, screenDimmingCategory), device-admin XML, manifest WRITE_SETTINGS and exported WidgetConfigActivity, Gradle plugin addition, and app version bump.

Changes

Cohort / File(s) Summary
Android Manifest
app/src/main/AndroidManifest.xml
Added android.permission.WRITE_SETTINGS; set android:exported="true" on .WidgetConfigActivity; normalized EOF newline.
Screen control (MainActivity)
app/src/main/java/com/immichframe/immichframe/MainActivity.kt
Persist/restore Settings.System.SCREEN_OFF_TIMEOUT; setScreenTimeout/applyScreenTimeout; dim/undim flow using 3s timeout when dimming; manage FLAG_KEEP_SCREEN_ON, short WakeLock to wake, dim overlay show/hide, snooze/redim scheduling, keyguard handling for API branches; restore original timeout on destroy.
Settings UI & fragment
app/src/main/res/xml/settings_view.xml, app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt
Added EditTextPreference screenTimeout and PreferenceCategory key screenDimmingCategory; added keepScreenOn switch; removed trailing question marks from several titles; dynamic visibility for dimming UI; input validation/toast for screenTimeout; preference-change listeners.
Device admin resource
app/src/main/res/xml/device_admin.xml
Added new device-admin XML declaring policy android.app.device_admin.DEVICE_ADMIN.
Build / Gradle
settings.gradle.kts, app/build.gradle.kts
Added org.gradle.toolchains.foojay-resolver-convention plugin in settings.gradle.kts; bumped android.defaultConfig.versionName from 1.0.48.01.0.48.2.

Sequence Diagram

sequenceDiagram
    autonumber
    actor User
    participant MainActivity
    participant SystemSettings as "Settings.System"
    participant PowerManager
    participant KeyguardManager
    participant WindowManager

    User->>MainActivity: trigger dim (screenDim = true)
    MainActivity->>WindowManager: clear FLAG_KEEP_SCREEN_ON
    MainActivity->>SystemSettings: read SCREEN_OFF_TIMEOUT
    SystemSettings-->>MainActivity: original timeout
    MainActivity->>SystemSettings: set SCREEN_OFF_TIMEOUT = 3000
    MainActivity->>WindowManager: show dim overlay / blank WebView
    MainActivity->>MainActivity: schedule snooze/redim timer

    User->>MainActivity: trigger undim (screenDim = false)
    MainActivity->>SystemSettings: restore original SCREEN_OFF_TIMEOUT
    MainActivity->>PowerManager: acquire short WakeLock
    PowerManager-->>MainActivity: device wakes
    MainActivity->>WindowManager: add FLAG_KEEP_SCREEN_ON, hide dim overlay
    alt API >= O_MR1
        MainActivity->>MainActivity: setShowWhenLocked / setTurnScreenOn
        MainActivity->>KeyguardManager: requestDismissKeyguard()
    else
        MainActivity->>MainActivity: set window flags to turn screen on / show when locked
        MainActivity->>KeyguardManager: requestDismissKeyguard()
    end
    KeyguardManager-->>MainActivity: keyguard dismissed / screen visible
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nudged the timeout down, then hopped to wake,

I saved the old timeout and let the dark screen take.
Permissions, plugin, widget exported with a cheer—
A tiny device-admin leaf planted near.
Hooray, the rabbit wakes the app when morning’s here!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "feat: screen turn-off" clearly summarizes the main change—converting screen dimming to actual screen turn-off with system timeout management—which is central to all file modifications in this PR.

✏️ 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/AndroidManifest.xml`:
- Around line 9-10: Remove the unused permission declaration for
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS from the Android manifest: delete the
<uses-permission
android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> entry
in AndroidManifest.xml and ensure no code references that permission string
elsewhere (search for "REQUEST_IGNORE_BATTERY_OPTIMIZATIONS") before committing
the change.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt`:
- Line 82: The current in-memory backup originalScreenTimeout is overwritten on
repeated screenDim(true) calls and lost on process death; make the backup
lifecycle-safe and idempotent by: only assign originalScreenTimeout when it is
the unset sentinel (e.g., -1) inside the method handling screenDim(true) so
repeated calls do not overwrite the real user value, persist the backup to
SharedPreferences when first saving it, and on restore (screenDim(false)) read
and apply the saved value only if a valid backup exists then clear both the
in-memory sentinel and the SharedPreferences key; also load any persisted backup
into originalScreenTimeout during startup (e.g., in onCreate) so crashes/process
death can still restore the user’s timeout.
- Around line 852-875: You set wake/unlock flags (setShowWhenLocked,
setTurnScreenOn, requestDismissKeyguard and window flags FLAG_TURN_SCREEN_ON /
FLAG_SHOW_WHEN_LOCKED / FLAG_DISMISS_KEYGUARD) but never clear them; update
MainActivity to explicitly reset these after the wake sequence completes (or in
onDestroy/onStop) by calling setShowWhenLocked(false) and setTurnScreenOn(false)
for API>=O_MR1, and clearing the window flags (window.clearFlags(...)) for the
older branches, and ensure any requestDismissKeyguard cleanup is handled; add
the cleanup logic to the same class (MainActivity) in a lifecycle method
(onDestroy or onStop) so the flags do not persist beyond the intended wake
period.
- Around line 779-788: The setScreenTimeout function calls
Settings.System.putInt without checking Settings.System.canWrite; before
attempting the write, check Settings.System.canWrite(contentResolver) and if it
returns false, avoid calling putInt and instead trigger the WRITE_SETTINGS flow
(e.g., launch an Intent with ACTION_MANAGE_WRITE_SETTINGS for the app) or
surface a user-facing prompt to request the permission; keep the existing
try/catch for putInt but only execute it when canWrite is true to prevent silent
failures and log or handle the denied-permission path accordingly.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95edefe and 1f65e55.

📒 Files selected for processing (3)
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/immichframe/immichframe/MainActivity.kt
  • settings.gradle.kts

private var previousImage: Helpers.ImageResponse? = null
private var currentImage: Helpers.ImageResponse? = null
private var portraitCache: Helpers.ImageResponse? = null
private var originalScreenTimeout: Int = -1
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make timeout backup lifecycle-safe and idempotent.

Line 82 keeps backup only in memory, and Lines 795-797 can overwrite it on repeated screenDim(true) calls (e.g., duplicate RPCs). That risks restoring 3000 instead of the user’s real value.

Proposed fix (idempotent backup/restore)
     private fun screenDim(dim: Boolean) {
+        val isCurrentlyDimmed = dimOverlay.isVisible
+        if (dim == isCurrentlyDimmed) return
+
         if (dim) {
             // save user's screen timeout and set to 3s to show "going to sleep" message
-            originalScreenTimeout = Settings.System.getInt(
-                contentResolver, Settings.System.SCREEN_OFF_TIMEOUT, 30000
-            )
+            if (originalScreenTimeout == -1) {
+                originalScreenTimeout = Settings.System.getInt(
+                    contentResolver, Settings.System.SCREEN_OFF_TIMEOUT, 30000
+                )
+            }
             setScreenTimeout(3000)
@@
             if (originalScreenTimeout != -1) {
                 setScreenTimeout(originalScreenTimeout)
+                originalScreenTimeout = -1
             }

Also persist the backup (e.g., SharedPreferences) so it can be restored after process death/crash.

Also applies to: 795-799, 829-831

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt` at line 82,
The current in-memory backup originalScreenTimeout is overwritten on repeated
screenDim(true) calls and lost on process death; make the backup lifecycle-safe
and idempotent by: only assign originalScreenTimeout when it is the unset
sentinel (e.g., -1) inside the method handling screenDim(true) so repeated calls
do not overwrite the real user value, persist the backup to SharedPreferences
when first saving it, and on restore (screenDim(false)) read and apply the saved
value only if a valid backup exists then clear both the in-memory sentinel and
the SharedPreferences key; also load any persisted backup into
originalScreenTimeout during startup (e.g., in onCreate) so crashes/process
death can still restore the user’s timeout.

Comment on lines +779 to +788
private fun setScreenTimeout(timeout: Int) {
try {
Settings.System.putInt(
contentResolver,
Settings.System.SCREEN_OFF_TIMEOUT,
timeout
)
} catch (e: Exception) {
Log.e("Settings", "Could not set screen timeout: ${e.message}")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify timeout writes and permission patterns
echo "=== Checking SCREEN_OFF_TIMEOUT and Settings API usage ==="
rg -n -C3 'SCREEN_OFF_TIMEOUT|Settings\.System\.putInt|Settings\.System\.canWrite|ACTION_MANAGE_WRITE_SETTINGS' app/src/main/java/com/immichframe/immichframe/MainActivity.kt

echo ""
echo "=== Checking AndroidManifest.xml for WRITE_SETTINGS permission ==="
rg -n 'WRITE_SETTINGS' app/src/main/AndroidManifest.xml

Repository: immichFrame/ImmichFrame_Android

Length of output: 953


🌐 Web query:

Android Settings.System.putInt WRITE_SETTINGS permission canWrite requirement documentation

💡 Result:

Settings.System.putInt() modifies values in the system settings table, so on Android 6.0 (API 23) and higher you must have the special WRITE_SETTINGS authorization (not a normal runtime permission prompt).

What the official docs say:

  • Declare the permission in the manifest: android.permission.WRITE_SETTINGS. If your app targets API 23+, the user must explicitly grant this via a management screen (Settings.ACTION_MANAGE_WRITE_SETTINGS). You can verify access with Settings.System.canWrite(context). [1]
  • Settings.System.canWrite(Context) “checks if the specified app can modify system settings” and reiterates the requirement: manifest declaration plus user grant via ACTION_MANAGE_WRITE_SETTINGS. [2]
  • This is considered a special permission that’s granted from Settings > Special app access, not via requestPermissions(). [3]

Minimal reference flow (API 23+):

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.System.canWrite(context)) {
  Intent i = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
  i.setData(Uri.parse("package:" + context.getPackageName()));
  context.startActivity(i);
} else {
  Settings.System.putInt(context.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, 100);
}

Sources:
[1] Android Manifest.permission.WRITE_SETTINGS reference (note about API 23+, manage screen, and canWrite()). (developer.android.com)
[2] Android Settings.System.canWrite() reference (requirements and ACTION_MANAGE_WRITE_SETTINGS). (developer.android.com)
[3] Android “Request special permissions” guide (special app access workflow). (developer.android.com)

Citations:


Guard SCREEN_OFF_TIMEOUT writes behind Settings.System.canWrite(...).

The WRITE_SETTINGS permission requires explicit user grant through device Settings on Android 6.0+. Without checking Settings.System.canWrite() before calling putInt(), these writes will fail when the user hasn't granted the permission, breaking the screen timeout feature.

Proposed fix
-    private fun setScreenTimeout(timeout: Int) {
+    private fun setScreenTimeout(timeout: Int): Boolean {
+        if (!Settings.System.canWrite(this)) {
+            Log.w("Settings", "WRITE_SETTINGS not granted; cannot set screen timeout")
+            return false
+        }
         try {
-            Settings.System.putInt(
+            return Settings.System.putInt(
                 contentResolver,
                 Settings.System.SCREEN_OFF_TIMEOUT,
                 timeout
             )
         } catch (e: Exception) {
             Log.e("Settings", "Could not set screen timeout: ${e.message}")
+            return false
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt` around lines
779 - 788, The setScreenTimeout function calls Settings.System.putInt without
checking Settings.System.canWrite; before attempting the write, check
Settings.System.canWrite(contentResolver) and if it returns false, avoid calling
putInt and instead trigger the WRITE_SETTINGS flow (e.g., launch an Intent with
ACTION_MANAGE_WRITE_SETTINGS for the app) or surface a user-facing prompt to
request the permission; keep the existing try/catch for putInt but only execute
it when canWrite is true to prevent silent failures and log or handle the
denied-permission path accordingly.

Comment on lines +852 to +875
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(false)
}
}
.start()
}

// turn screen back on, dismissing keyguard lockscreen
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)
setTurnScreenOn(true)
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
keyguardManager.requestDismissKeyguard(this, null)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
keyguardManager.requestDismissKeyguard(this, null)
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)

} else {
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n -C3 'setTurnScreenOn\(|setShowWhenLocked\(|FLAG_TURN_SCREEN_ON|FLAG_SHOW_WHEN_LOCKED|FLAG_DISMISS_KEYGUARD|addFlags|clearFlags' app/src/main/java/com/immichframe/immichframe/MainActivity.kt

Repository: immichFrame/ImmichFrame_Android

Length of output: 3420


🏁 Script executed:

rg -n 'onPause|onDestroy|onStop' app/src/main/java/com/immichframe/immichframe/MainActivity.kt | head -20

Repository: immichFrame/ImmichFrame_Android

Length of output: 209


🏁 Script executed:

sed -n '924,940p' app/src/main/java/com/immichframe/immichframe/MainActivity.kt

Repository: immichFrame/ImmichFrame_Android

Length of output: 516


Clear wake/unlock flags after wake completes.

You set multiple wake/unlock flags, but only partially reset them. In the O_MR1 branch, setShowWhenLocked(false) is called but setTurnScreenOn(true) persists. In the Android 8.0 and earlier branches, the window flags are never cleared. These flags remain active throughout the activity lifecycle with no cleanup in onDestroy(), potentially causing unintended screen wake behavior later.

Proposed fix
                     .withEndAction {
                         dimOverlay.visibility = View.GONE
                         loadSettings()
                         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
                             setShowWhenLocked(false)
+                            setTurnScreenOn(false)
+                        } else {
+                            window.clearFlags(
+                                WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
+                                    WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
+                                    WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+                            )
                         }
                     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(false)
}
}
.start()
}
// turn screen back on, dismissing keyguard lockscreen
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)
setTurnScreenOn(true)
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
keyguardManager.requestDismissKeyguard(this, null)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
keyguardManager.requestDismissKeyguard(this, null)
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(false)
setTurnScreenOn(false)
} else {
window.clearFlags(
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
)
}
}
.start()
}
// turn screen back on, dismissing keyguard lockscreen
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)
setTurnScreenOn(true)
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
keyguardManager.requestDismissKeyguard(this, null)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
keyguardManager.requestDismissKeyguard(this, null)
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt` around lines
852 - 875, You set wake/unlock flags (setShowWhenLocked, setTurnScreenOn,
requestDismissKeyguard and window flags FLAG_TURN_SCREEN_ON /
FLAG_SHOW_WHEN_LOCKED / FLAG_DISMISS_KEYGUARD) but never clear them; update
MainActivity to explicitly reset these after the wake sequence completes (or in
onDestroy/onStop) by calling setShowWhenLocked(false) and setTurnScreenOn(false)
for API>=O_MR1, and clearing the window flags (window.clearFlags(...)) for the
older branches, and ensure any requestDismissKeyguard cleanup is handled; add
the cleanup logic to the same class (MainActivity) in a lifecycle method
(onDestroy or onStop) so the flags do not persist beyond the intended wake
period.

@JW-CH JW-CH requested a review from 3rob3 March 2, 2026 08:08
@3rob3
Copy link
Copy Markdown
Contributor

3rob3 commented Mar 2, 2026

Thanks for the PR! While I appreciate the effort, I cannot merge these changes because this app goes through the Google Play Store. Modifying system-level settings and using privileged APIs introduces several risks that would likely trigger Google's Automated Scanners and lead to a Play Store rejection:

  • Privileged Access Abuse: Requesting WRITE_SETTINGS to modify SCREEN_OFF_TIMEOUT is considered "Special App Access." Google's automated scanners flag this unless it is the core functionality of the app (like a system utility).
  • System Integrity Risk: This code modifies a global system value. If the app crashes after setting the timeout to 3 seconds but before restoring it, the user's device becomes effectively unusable until they manually find and fix the setting.
  • Special Permissions
  • Policy Non-Compliance: Using deprecated WakeLock flags and programmatically dismissing the keyguard is a common reason for "Malicious Behavior" flags during the automated review process (I have had this happen already).

Once it is flagged the whole app must be approved again which is a nightmare. If you wanted to maintain this branch (and fix the system integrity issue above), we could put a note in our docs that for full screen off support see LINK_TO_YOUR_REPO?
Thanks again for the contribution!

@dwolstenholme13
Copy link
Copy Markdown
Author

dwolstenholme13 commented Mar 4, 2026 via email

@3rob3
Copy link
Copy Markdown
Contributor

3rob3 commented Mar 5, 2026

This app was made long before we started using Frameo devices, then I adapted it to support them. Believe it or not it is most popular (by far) on Android TV devices. This is likely due to the screensaver functionality. I have been considering splitting them (would make maintaining the Play store version much simpler pulling out all the non webview code), but then I would have to maintain two branches, and I just don't have the time for that.

I don't understand your question/comment about a 3 second timeout. Are you talking about the Keep Screen On toggle? If so no that is not at odds with the screen dim. Keep screen on is so the device doesn't auto turn off, thereby locking the device.

@dwolstenholme13
Copy link
Copy Markdown
Author

dwolstenholme13 commented Mar 6, 2026 via email

@3rob3
Copy link
Copy Markdown
Contributor

3rob3 commented Mar 6, 2026

KeepScreenOn can probably be removed (this is the default behavior in all other clients). You are right, without that set screen dimming is kinda useless.

…ection to keep screen on is set, and if it is not set, added selection to set timeout value in any number of minutes rather than requiring user to set it in Android settings.

Fix so user timeout value not lost
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.

Actionable comments posted: 4

♻️ Duplicate comments (3)
app/src/main/java/com/immichframe/immichframe/MainActivity.kt (3)

780-790: ⚠️ Potential issue | 🟠 Major

Check Settings.System.canWrite(...) before every timeout write.

Declaring WRITE_SETTINGS is not enough on Android 6.0+. Without the special-access grant these putInt() calls fail, so the timeout feature silently stops working unless you surface ACTION_MANAGE_WRITE_SETTINGS first.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt` around lines
780 - 790, In setScreenTimeout(timeout: Int) ensure you call
Settings.System.canWrite(context) before Settings.System.putInt — if it returns
false, launch the ACTION_MANAGE_WRITE_SETTINGS intent to prompt the user for the
special WRITE_SETTINGS grant (or otherwise surface an error/notification) and
skip/abort the putInt call; update the catch to handle/re-log refusal. Target
the setScreenTimeout function and the surrounding code that invokes
Settings.System.putInt to implement this permission check and user flow.

878-900: ⚠️ Potential issue | 🟠 Major

Clear the temporary wake and unlock flags after wake completes.

The O_MR1+ path only calls setShowWhenLocked(false), so setTurnScreenOn(true) stays latched. The pre-O window flags are never cleared at all. Later resumes can then wake or unlock the device unexpectedly.

Suggested fix
                     .withEndAction {
                         dimOverlay.visibility = View.GONE
                         loadSettings()
                         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
                             setShowWhenLocked(false)
+                            setTurnScreenOn(false)
+                        } else {
+                            window.clearFlags(
+                                WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
+                                    WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
+                                    WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+                            )
                         }
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt` around lines
878 - 900, The wake/unlock flags are never cleared which leaves
setTurnScreenOn(true) and the window flags latched; update MainActivity so that
after the wake/unlock sequence completes (e.g., after the start()/animation or
once keyguard dismissal finishes) you clear the flags: for API >= O_MR1 call
setTurnScreenOn(false) and setShowWhenLocked(false); for API >= O and older call
window.clearFlags(...) to remove WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
FLAG_SHOW_WHEN_LOCKED and FLAG_DISMISS_KEYGUARD as appropriate (while keeping
the keyguardManager.requestDismissKeyguard(...) call). Ensure these cleanup
calls run reliably after the wake completes.

82-82: ⚠️ Potential issue | 🟠 Major

Persist and eagerly restore the original timeout.

originalScreenTimeout still lives only in memory, and applyScreenTimeout() never restores it when keepScreenOn flips back to true. A crash or process kill after setScreenTimeout(...), or simply re-enabling keepScreenOn, can leave the device stuck on the app-specific global timeout. Persist the backup, load it on startup, and funnel every restore path through one helper.

Also applies to: 797-808, 818-856, 951-954

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt` at line 82,
originalScreenTimeout is only in-memory and
applyScreenTimeout()/setScreenTimeout(...) doesn't reliably restore it when
keepScreenOn toggles or after process restart; persist the original timeout
(e.g., SharedPreferences) when you first back it up, load it during startup (in
MainActivity.onCreate) to repopulate originalScreenTimeout, and centralize all
restore logic into a single helper method (e.g., restoreOriginalScreenTimeout())
that both applyScreenTimeout() and the keepScreenOn toggle call; ensure every
code path that wants to restore uses that helper and that the helper clears the
persisted backup after successful restore.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/AndroidManifest.xml`:
- Around line 67-70: WidgetConfigActivity currently proceeds to read/write
widget preferences without validating the incoming EXTRA_APPWIDGET_ID; add an
early guard in WidgetConfigActivity (after extracting appWidgetId from the
intent) that checks if appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID and
if so calls setResult(RESULT_CANCELED), finish(), and returns to prevent further
preference access and accidental RESULT_OK for invalid requests.
- Line 9: Remove the unused WRITE_SETTINGS permission declaration from
AndroidManifest.xml: delete the line declaring android.permission.WRITE_SETTINGS
since the app uses WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON for screen
timeout and does not interact with system Settings; if you actually need to
modify system settings instead, implement that logic where appropriate and add
the corresponding rationale and documentation, otherwise simply remove the
<uses-permission android:name="android.permission.WRITE_SETTINGS" /> entry.

In `@app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt`:
- Around line 62-67: When chkKeepScreenOn preference is turned off, you must
also clear the persisted screen dimming flag and ensure
MainActivity.loadSettings() ignores/stops any schedule when keepScreenOn is
false: in the chkKeepScreenOn.setOnPreferenceChangeListener block (symbols:
chkKeepScreenOn, screenDimmingCategory, txtScreenTimeout) write the preference
change to remove or set the stored "screenDim" flag to false via the app
SharedPreferences (e.g., sharedPreferences.edit().remove("screenDim") or
.putBoolean("screenDim", false).apply()), and in MainActivity.loadSettings()
gate reading/activation of the screen dimming schedule by first checking the
current keepScreenOn value (do not rely on a stale screenDim value when
keepScreenOn is false) so the schedule is not resumed if keepScreenOn is
disabled.
- Around line 69-78: The preference listener in SettingsFragment.kt accepts any
positive integer which can overflow when MainActivity.applyScreenTimeout()
multiplies minutes by 60*1000; clamp the parsed value to a safe maximum before
accepting it: compute a maxMinutes based on the same timeout unit used in
MainActivity (e.g., derive maxMinutes = SCREEN_OFF_TIMEOUT / (60*1000) or use a
named constant) and validate newValue.toString().toIntOrNull() to be between 1
and maxMinutes; if out of range, show the existing Toast and return false,
otherwise return true. Reference txtScreenTimeout preference listener in
SettingsFragment and MainActivity.applyScreenTimeout / SCREEN_OFF_TIMEOUT to
keep limits consistent.

---

Duplicate comments:
In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt`:
- Around line 780-790: In setScreenTimeout(timeout: Int) ensure you call
Settings.System.canWrite(context) before Settings.System.putInt — if it returns
false, launch the ACTION_MANAGE_WRITE_SETTINGS intent to prompt the user for the
special WRITE_SETTINGS grant (or otherwise surface an error/notification) and
skip/abort the putInt call; update the catch to handle/re-log refusal. Target
the setScreenTimeout function and the surrounding code that invokes
Settings.System.putInt to implement this permission check and user flow.
- Around line 878-900: The wake/unlock flags are never cleared which leaves
setTurnScreenOn(true) and the window flags latched; update MainActivity so that
after the wake/unlock sequence completes (e.g., after the start()/animation or
once keyguard dismissal finishes) you clear the flags: for API >= O_MR1 call
setTurnScreenOn(false) and setShowWhenLocked(false); for API >= O and older call
window.clearFlags(...) to remove WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
FLAG_SHOW_WHEN_LOCKED and FLAG_DISMISS_KEYGUARD as appropriate (while keeping
the keyguardManager.requestDismissKeyguard(...) call). Ensure these cleanup
calls run reliably after the wake completes.
- Line 82: originalScreenTimeout is only in-memory and
applyScreenTimeout()/setScreenTimeout(...) doesn't reliably restore it when
keepScreenOn toggles or after process restart; persist the original timeout
(e.g., SharedPreferences) when you first back it up, load it during startup (in
MainActivity.onCreate) to repopulate originalScreenTimeout, and centralize all
restore logic into a single helper method (e.g., restoreOriginalScreenTimeout())
that both applyScreenTimeout() and the keepScreenOn toggle call; ensure every
code path that wants to restore uses that helper and that the helper clears the
persisted backup after successful restore.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ea726cff-9067-4560-be55-c9d2e9b07fa8

📥 Commits

Reviewing files that changed from the base of the PR and between 1f65e55 and d12f17f.

📒 Files selected for processing (7)
  • app/build.gradle.kts
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/immichframe/immichframe/MainActivity.kt
  • app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt
  • app/src/main/res/xml/device_admin.xml
  • app/src/main/res/xml/settings_view.xml
  • gradlew
✅ Files skipped from review due to trivial changes (2)
  • app/src/main/res/xml/device_admin.xml
  • app/build.gradle.kts

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "(build\.gradle|AndroidManifest|flavor)" | head -20

Repository: immichFrame/ImmichFrame_Android

Length of output: 145


🏁 Script executed:

fd -t f "AndroidManifest" -o "build.gradle"

Repository: immichFrame/ImmichFrame_Android

Length of output: 218


🏁 Script executed:

cd app && find . -name "AndroidManifest.xml" -o -name "build.gradle*" | head -20

Repository: immichFrame/ImmichFrame_Android

Length of output: 124


🏁 Script executed:

cat -n app/build.gradle.kts | head -100

Repository: immichFrame/ImmichFrame_Android

Length of output: 3658


🏁 Script executed:

cat -n app/src/main/AndroidManifest.xml

Repository: immichFrame/ImmichFrame_Android

Length of output: 4149


🏁 Script executed:

rg "WRITE_SETTINGS" -B 3 -A 3

Repository: immichFrame/ImmichFrame_Android

Length of output: 663


🏁 Script executed:

rg "WRITE_SETTINGS\|Settings\.ACTION_MANAGE_WRITE_SETTINGS" app/src -B 2 -A 2

Repository: immichFrame/ImmichFrame_Android

Length of output: 57


🏁 Script executed:

grep -r "flavors\|productFlavors\|flavorDimensions" app/build.gradle.kts

Repository: immichFrame/ImmichFrame_Android

Length of output: 57


🏁 Script executed:

find app/src -name "*.kt" -o -name "*.java" | xargs grep -l "WRITE_SETTINGS\|Settings\|ACTION_MANAGE_WRITE_SETTINGS" 2>/dev/null

Repository: immichFrame/ImmichFrame_Android

Length of output: 456


🏁 Script executed:

cat app/src/main/java/com/immichframe/immichframe/SettingsActivity.kt

Repository: immichFrame/ImmichFrame_Android

Length of output: 494


🏁 Script executed:

cat app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt

Repository: immichFrame/ImmichFrame_Android

Length of output: 7814


🏁 Script executed:

rg "ACTION_MANAGE_WRITE_SETTINGS\|canWrite\|WRITE_SETTINGS" app/src/main/java --type kt -B 3 -A 3

Repository: immichFrame/ImmichFrame_Android

Length of output: 101


🏁 Script executed:

rg "ACTION_MANAGE_WRITE_SETTINGS|canWrite|WRITE_SETTINGS" app/src/main/java

Repository: immichFrame/ImmichFrame_Android

Length of output: 57


🏁 Script executed:

rg "keepScreenOn|screenTimeout|dim" app/src/main/java --type-add "kt:*.kt" -B 1 -A 1

Repository: immichFrame/ImmichFrame_Android

Length of output: 11002


🏁 Script executed:

cat app/src/main/java/com/immichframe/immichframe/ScreenSaverService.kt

Repository: immichFrame/ImmichFrame_Android

Length of output: 23994


Remove unused WRITE_SETTINGS permission.

WRITE_SETTINGS is declared in the manifest but not used anywhere in the codebase. Screen timeout management is handled via app-level WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, not system Settings. This unused permission declaration will likely trigger Play Store policy review or rejection. Either remove it or implement and document its actual usage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/AndroidManifest.xml` at line 9, Remove the unused WRITE_SETTINGS
permission declaration from AndroidManifest.xml: delete the line declaring
android.permission.WRITE_SETTINGS since the app uses
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON for screen timeout and does not
interact with system Settings; if you actually need to modify system settings
instead, implement that logic where appropriate and add the corresponding
rationale and documentation, otherwise simply remove the <uses-permission
android:name="android.permission.WRITE_SETTINGS" /> entry.

Comment on lines 67 to +70
<activity android:name=".WidgetConfigActivity"
android:label="Widget Configuration"
android:theme="@style/Theme.WidgetConfigDialog" />
android:theme="@style/Theme.WidgetConfigDialog"
android:exported="true" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'EXTRA_APPWIDGET_ID|INVALID_APPWIDGET_ID|RESULT_CANCELED|RESULT_OK|widgetBackground' app/src/main/java/com/immichframe/immichframe/WidgetConfigActivity.kt

Repository: immichFrame/ImmichFrame_Android

Length of output: 1048


🏁 Script executed:

cat app/src/main/java/com/immichframe/immichframe/WidgetConfigActivity.kt

Repository: immichFrame/ImmichFrame_Android

Length of output: 2214


Reject invalid appWidgetId before accessing preferences.

WidgetConfigActivity reads and writes widget preferences without validating EXTRA_APPWIDGET_ID. If called by an external app without providing a valid widget ID, the activity defaults to INVALID_APPWIDGET_ID and proceeds to access shared preferences anyway, returning RESULT_OK regardless. Add an early check to return RESULT_CANCELED and finish if the widget ID is invalid:

if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
    setResult(RESULT_CANCELED)
    finish()
    return
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/AndroidManifest.xml` around lines 67 - 70, WidgetConfigActivity
currently proceeds to read/write widget preferences without validating the
incoming EXTRA_APPWIDGET_ID; add an early guard in WidgetConfigActivity (after
extracting appWidgetId from the intent) that checks if appWidgetId ==
AppWidgetManager.INVALID_APPWIDGET_ID and if so calls
setResult(RESULT_CANCELED), finish(), and returns to prevent further preference
access and accidental RESULT_OK for invalid requests.

Comment on lines +62 to +67
chkKeepScreenOn?.setOnPreferenceChangeListener { _, newValue ->
val value = newValue as Boolean
screenDimmingCategory?.isVisible = value
txtScreenTimeout?.isVisible = !value
true
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Disable screenDim when keepScreenOn is turned off.

This only hides the category. MainActivity.loadSettings() still honors the persisted screenDim flag, so a previously enabled schedule keeps running while the option is invisible. Clear the flag here, and also gate the read in MainActivity so stale prefs from older installs do not survive.

Suggested fix
 chkKeepScreenOn?.setOnPreferenceChangeListener { _, newValue ->
     val value = newValue as Boolean
     screenDimmingCategory?.isVisible = value
     txtScreenTimeout?.isVisible = !value
+    if (!value) {
+        chkScreenDim?.isChecked = false
+        txtDimTime?.isVisible = false
+    }
     true
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
chkKeepScreenOn?.setOnPreferenceChangeListener { _, newValue ->
val value = newValue as Boolean
screenDimmingCategory?.isVisible = value
txtScreenTimeout?.isVisible = !value
true
}
chkKeepScreenOn?.setOnPreferenceChangeListener { _, newValue ->
val value = newValue as Boolean
screenDimmingCategory?.isVisible = value
txtScreenTimeout?.isVisible = !value
if (!value) {
chkScreenDim?.isChecked = false
txtDimTime?.isVisible = false
}
true
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt` around
lines 62 - 67, When chkKeepScreenOn preference is turned off, you must also
clear the persisted screen dimming flag and ensure MainActivity.loadSettings()
ignores/stops any schedule when keepScreenOn is false: in the
chkKeepScreenOn.setOnPreferenceChangeListener block (symbols: chkKeepScreenOn,
screenDimmingCategory, txtScreenTimeout) write the preference change to remove
or set the stored "screenDim" flag to false via the app SharedPreferences (e.g.,
sharedPreferences.edit().remove("screenDim") or .putBoolean("screenDim",
false).apply()), and in MainActivity.loadSettings() gate reading/activation of
the screen dimming schedule by first checking the current keepScreenOn value (do
not rely on a stale screenDim value when keepScreenOn is false) so the schedule
is not resumed if keepScreenOn is disabled.

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.

Actionable comments posted: 2

Caution

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

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

523-525: ⚠️ Potential issue | 🟠 Major

Deduplicate dimCheckRunnable scheduling.

loadSettings() runs on startup, after returning from settings, and again after undim. Posting dimCheckRunnable each time without removing the previous callback creates multiple self-rescheduling loops, so dim checks multiply over time. Remove the callback before every post(...) or guard scheduling with a dedicated flag.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt` around lines
523 - 525, The dimCheckRunnable is being posted repeatedly (from loadSettings
and other flows) causing duplicate self-rescheduling loops; before calling
handler.post(dimCheckRunnable) in loadSettings (and any other place that
schedules it), call handler.removeCallbacks(dimCheckRunnable) to clear existing
callbacks, or alternatively introduce and check a boolean (e.g.,
isDimRunnableScheduled) that you set true when posting and false when removing
to prevent double-scheduling; update the scheduling logic around screenDim,
handler.post(dimCheckRunnable), and wherever dimCheckRunnable is
removed/rescheduled so only one active loop exists.
♻️ Duplicate comments (2)
app/src/main/java/com/immichframe/immichframe/MainActivity.kt (2)

879-901: ⚠️ Potential issue | 🟠 Major

Clear all wake/unlock flags after the wake completes.

This still leaves setTurnScreenOn(true) latched on API 27+, and the pre-27 branches never clear FLAG_TURN_SCREEN_ON, FLAG_SHOW_WHEN_LOCKED, or FLAG_DISMISS_KEYGUARD. Those flags persist past this wake cycle and can cause later resumes to wake/unlock unexpectedly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt` around lines
879 - 901, The wake/unlock flags are being set (setTurnScreenOn,
setShowWhenLocked and window.addFlags with
FLAG_TURN_SCREEN_ON/FLAG_SHOW_WHEN_LOCKED/FLAG_DISMISS_KEYGUARD) but never
cleared, leaving the device latched awake; after the wake completes (e.g., at
the end of the wake/animation completion handler where you currently call
.start() or after requestDismissKeyguard succeeds) explicitly clear them: for
API >= O_MR1 call setTurnScreenOn(false) and setShowWhenLocked(false); for older
APIs call window.clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD) so the flags do not persist
across resumes.

82-82: ⚠️ Potential issue | 🟠 Major

Make the SCREEN_OFF_TIMEOUT override survivable across failed writes and process loss.

originalScreenTimeout still only lives in memory, setScreenTimeout() gives callers no success signal, and Lines 855-857 clear the backup as if the restore succeeded. If the system write is rejected or the process dies while the 3s/custom timeout is active, the device-global timeout can be stranded with no value left to restore. Persist the original timeout before the first write, gate writes behind Settings.System.canWrite(...), and only clear/reset the backup after a confirmed restore.

Also applies to: 781-790, 803-809, 819-824, 855-857, 953-956

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt` at line 82,
Persist the originalScreenTimeout to durable storage (e.g. SharedPreferences)
before performing the first write to Settings.System.SCREEN_OFF_TIMEOUT, require
Settings.System.canWrite(...) to be true before any write, change
setScreenTimeout(...) to provide a boolean success/failure result (or throw) so
callers can react to failed writes, and only clear the persisted backup after a
confirmed successful restore in the restore path (do not clear in the code paths
that assume success). Locate and update the fields/methods
originalScreenTimeout, setScreenTimeout, and the restore routine that currently
clears the backup (the code around the restoreOriginalTimeout/clear backup
logic) to read/write the persisted backup and to keep it intact on write/restore
failures or process death so the system timeout can always be recovered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt`:
- Line 815: The short-circuit check "if (dim == dimOverlay.isVisible) return"
prevents reapplying the sleep/FLAG_KEEP_SCREEN_ON state while the dim overlay is
visible; change the condition so it only returns when there is truly no change
and the overlay is not active (e.g., only return when dim ==
dimOverlay.isVisible && !dimOverlay.isVisible), so onResume() (and the other
affected block around the applyScreenTimeout() calls) will still reassert the
configured short timeout during an active dim session; update both the check at
onResume() (where applyScreenTimeout() is called) and the similar check in the
block around lines 940-944 to use this refined condition.
- Around line 817-824: The current dim flow uses Handler.postDelayed in
checkDimTime()/dim loop and reduces the timeout via setScreenTimeout(3000), but
the Handler stops when the device sleeps so screenDim(false) may never run;
replace the periodic Handler scheduling with an AlarmManager
RTC_WAKEUP/ELAPSED_REALTIME_WAKEUP scheduled for the exact end of the dim window
so the system will wake and run your undim logic. Specifically: when entering
dim mode (where originalScreenTimeout is captured and setScreenTimeout(3000) is
called), schedule an AlarmManager alarm for the dim-end timestamp; cancel that
alarm when leaving dim early; in the alarm receiver or PendingIntent invoke the
same code path currently used for undimming (call screenDim(false) or the
existing wake-and-undim logic) and ensure any WakeLock acquisition remains in
that undim path; remove or stop relying on Handler.postDelayed/checkDimTime for
the final wake-up.

---

Outside diff comments:
In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt`:
- Around line 523-525: The dimCheckRunnable is being posted repeatedly (from
loadSettings and other flows) causing duplicate self-rescheduling loops; before
calling handler.post(dimCheckRunnable) in loadSettings (and any other place that
schedules it), call handler.removeCallbacks(dimCheckRunnable) to clear existing
callbacks, or alternatively introduce and check a boolean (e.g.,
isDimRunnableScheduled) that you set true when posting and false when removing
to prevent double-scheduling; update the scheduling logic around screenDim,
handler.post(dimCheckRunnable), and wherever dimCheckRunnable is
removed/rescheduled so only one active loop exists.

---

Duplicate comments:
In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt`:
- Around line 879-901: The wake/unlock flags are being set (setTurnScreenOn,
setShowWhenLocked and window.addFlags with
FLAG_TURN_SCREEN_ON/FLAG_SHOW_WHEN_LOCKED/FLAG_DISMISS_KEYGUARD) but never
cleared, leaving the device latched awake; after the wake completes (e.g., at
the end of the wake/animation completion handler where you currently call
.start() or after requestDismissKeyguard succeeds) explicitly clear them: for
API >= O_MR1 call setTurnScreenOn(false) and setShowWhenLocked(false); for older
APIs call window.clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD) so the flags do not persist
across resumes.
- Line 82: Persist the originalScreenTimeout to durable storage (e.g.
SharedPreferences) before performing the first write to
Settings.System.SCREEN_OFF_TIMEOUT, require Settings.System.canWrite(...) to be
true before any write, change setScreenTimeout(...) to provide a boolean
success/failure result (or throw) so callers can react to failed writes, and
only clear the persisted backup after a confirmed successful restore in the
restore path (do not clear in the code paths that assume success). Locate and
update the fields/methods originalScreenTimeout, setScreenTimeout, and the
restore routine that currently clears the backup (the code around the
restoreOriginalTimeout/clear backup logic) to read/write the persisted backup
and to keep it intact on write/restore failures or process death so the system
timeout can always be recovered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec2ccae8-b297-4808-897f-2d5ebd868c31

📥 Commits

Reviewing files that changed from the base of the PR and between d12f17f and 2d83d95.

📒 Files selected for processing (2)
  • app/src/main/java/com/immichframe/immichframe/MainActivity.kt
  • app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt

Comment on lines 817 to +824
if (dim) {
lp.screenBrightness = 0.01f
window.attributes = lp
// save user's screen timeout and set to 3s to show "going to sleep" message
if (originalScreenTimeout == -1) {
originalScreenTimeout = Settings.System.getInt(
contentResolver, Settings.System.SCREEN_OFF_TIMEOUT, 30000
)
}
setScreenTimeout(3000)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's find the MainActivity.kt file
find . -name "MainActivity.kt" -type f

Repository: immichFrame/ImmichFrame_Android

Length of output: 138


🏁 Script executed:

#!/bin/bash
# Get file size first
wc -l ./app/src/main/java/com/immichframe/immichframe/MainActivity.kt

Repository: immichFrame/ImmichFrame_Android

Length of output: 142


🏁 Script executed:

#!/bin/bash
# Read the context around lines 817-824
sed -n '800,840p' ./app/src/main/java/com/immichframe/immichframe/MainActivity.kt

Repository: immichFrame/ImmichFrame_Android

Length of output: 1786


🏁 Script executed:

#!/bin/bash
# Read the context around lines 850-910
sed -n '850,910p' ./app/src/main/java/com/immichframe/immichframe/MainActivity.kt

Repository: immichFrame/ImmichFrame_Android

Length of output: 2914


🏁 Script executed:

#!/bin/bash
# Search for screenDim calls and how they're scheduled
rg "screenDim" ./app/src/main/java/com/immichframe/immichframe/MainActivity.kt -A 2 -B 2

Repository: immichFrame/ImmichFrame_Android

Length of output: 1104


🏁 Script executed:

#!/bin/bash
# Search for dimCheckRunnable definition
rg "dimCheckRunnable" ./app/src/main/java/com/immichframe/immichframe/MainActivity.kt -A 10 -B 2

Repository: immichFrame/ImmichFrame_Android

Length of output: 1004


🏁 Script executed:

#!/bin/bash
# Let me verify the complete flow by checking when setScreenTimeout(3000) actually lets device sleep
# and whether there's any other wake-up mechanism
rg "setScreenTimeout|AlarmManager|WakeLock" ./app/src/main/java/com/immichframe/immichframe/MainActivity.kt -B 2 -A 2

Repository: immichFrame/ImmichFrame_Android

Length of output: 1369


Replace Handler-based scheduling with AlarmManager for reliable undim triggering.

The dim-to-undim flow relies on Handler.postDelayed() to periodically call checkDimTime() and trigger screenDim(false). However, once the device sleeps after 3 seconds (line 824 sets setScreenTimeout(3000)), the Handler loop stops executing—it is not a wake source. The device will remain asleep until something else wakes it, making the end-of-window auto-undim unreliable.

While the screenDim(false) code path (lines 863-869) correctly uses WakeLock with ACQUIRE_CAUSES_WAKEUP to wake the device, this code never executes if screenDim(false) is never called. To ensure reliable undim at the scheduled end time, use AlarmManager to schedule a wake-up alarm at the end of the dim window instead of relying on the Handler loop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt` around lines
817 - 824, The current dim flow uses Handler.postDelayed in checkDimTime()/dim
loop and reduces the timeout via setScreenTimeout(3000), but the Handler stops
when the device sleeps so screenDim(false) may never run; replace the periodic
Handler scheduling with an AlarmManager RTC_WAKEUP/ELAPSED_REALTIME_WAKEUP
scheduled for the exact end of the dim window so the system will wake and run
your undim logic. Specifically: when entering dim mode (where
originalScreenTimeout is captured and setScreenTimeout(3000) is called),
schedule an AlarmManager alarm for the dim-end timestamp; cancel that alarm when
leaving dim early; in the alarm receiver or PendingIntent invoke the same code
path currently used for undimming (call screenDim(false) or the existing
wake-and-undim logic) and ensure any WakeLock acquisition remains in that undim
path; remove or stop relying on Handler.postDelayed/checkDimTime for the final
wake-up.

… where the user can press the power button while the screen is turned off in dimming mode, and the screen will come back on for 30s, then turn back off.
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.

♻️ Duplicate comments (1)
app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt (1)

43-48: ⚠️ Potential issue | 🟠 Major

Handle stale screenDim on initial load when keepScreenOn is already false.

On Line 43–48, visibility is toggled, but persisted screenDim is not reset on first render. If older prefs still have screenDim=true, MainActivity can still resume dim scheduling while the option is hidden. This is the same concern previously raised and is still partially open in this path.

Proposed fix
 val keepScreenOn = chkKeepScreenOn?.isChecked ?: false
 screenDimmingCategory?.isVisible = keepScreenOn
 txtScreenTimeout?.isVisible = !keepScreenOn
+if (!keepScreenOn) {
+    chkScreenDim?.isChecked = false
+    txtDimTime?.isVisible = false
+}
 
 val screenDim = chkScreenDim?.isChecked ?: false
 txtDimTime?.isVisible = screenDim
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt` around
lines 43 - 48, When initializing visibility in SettingsFragment, currently
txtDimTime visibility uses the persisted chkScreenDim value even when
keepScreenOn is false; update the block that reads chkKeepScreenOn/chkScreenDim
(references: chkKeepScreenOn, chkScreenDim, screenDimmingCategory,
txtScreenTimeout, txtDimTime) so that when keepScreenOn is true you restore
screenDim visibility from prefs, but when keepScreenOn is false you explicitly
set screenDim to false, persist that change (update the preference backing
chkScreenDim) and notify/call the existing MainActivity dim scheduling cancel
method so any scheduled dimming is stopped; ensure UI elements
(screenDimmingCategory, txtScreenTimeout, txtDimTime) reflect the cleared state.
🧹 Nitpick comments (1)
app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt (1)

155-157: Reuse the existing txtDimTime reference instead of re-fetching the same preference.

This is minor, but it reduces duplication and keeps preference wiring in one place.

Proposed refactor
- val timePref = findPreference<EditTextPreference>("dim_time_range")
- timePref?.setOnPreferenceChangeListener { _, newValue ->
+ txtDimTime?.setOnPreferenceChangeListener { _, newValue ->
     val timeRange = newValue.toString().trim()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt` around
lines 155 - 157, The listener re-fetches the same EditTextPreference instead of
reusing the existing txtDimTime reference; update the
setOnPreferenceChangeListener block to use the already-declared txtDimTime
(instead of calling findPreference<EditTextPreference>("dim_time_range") again)
so the code uses the single preference instance (txtDimTime) when
trimming/validating newValue and setting values, keeping preference wiring
centralized in the txtDimTime variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt`:
- Around line 43-48: When initializing visibility in SettingsFragment, currently
txtDimTime visibility uses the persisted chkScreenDim value even when
keepScreenOn is false; update the block that reads chkKeepScreenOn/chkScreenDim
(references: chkKeepScreenOn, chkScreenDim, screenDimmingCategory,
txtScreenTimeout, txtDimTime) so that when keepScreenOn is true you restore
screenDim visibility from prefs, but when keepScreenOn is false you explicitly
set screenDim to false, persist that change (update the preference backing
chkScreenDim) and notify/call the existing MainActivity dim scheduling cancel
method so any scheduled dimming is stopped; ensure UI elements
(screenDimmingCategory, txtScreenTimeout, txtDimTime) reflect the cleared state.

---

Nitpick comments:
In `@app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt`:
- Around line 155-157: The listener re-fetches the same EditTextPreference
instead of reusing the existing txtDimTime reference; update the
setOnPreferenceChangeListener block to use the already-declared txtDimTime
(instead of calling findPreference<EditTextPreference>("dim_time_range") again)
so the code uses the single preference instance (txtDimTime) when
trimming/validating newValue and setting values, keeping preference wiring
centralized in the txtDimTime variable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b899fcc9-3513-4bd6-91b5-c54e18afc85a

📥 Commits

Reviewing files that changed from the base of the PR and between 2d83d95 and 0be4a56.

📒 Files selected for processing (2)
  • app/src/main/java/com/immichframe/immichframe/MainActivity.kt
  • app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/immichframe/immichframe/MainActivity.kt

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt`:
- Around line 158-159: Replace the misspelled reference txtFimeTime with the
correctly declared txtDimTime in the preference change hook so the dimming time
validation runs; update the call to setOnPreferenceChangeListener from
txtFimeTime?.setOnPreferenceChangeListener { ... } to
txtDimTime?.setOnPreferenceChangeListener { _, newValue -> ... } (keeping the
existing listener logic intact).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: faef687f-df70-4b56-a9b0-f8da5a6b98a5

📥 Commits

Reviewing files that changed from the base of the PR and between 0be4a56 and 0d11e2d.

📒 Files selected for processing (1)
  • app/src/main/java/com/immichframe/immichframe/SettingsFragment.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.

2 participants