Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ android {
minSdk = 23
targetSdk = 36
versionCode = 48
versionName = "1.0.48.0"
versionName = "1.0.48.2"
}

buildTypes {
Expand Down Expand Up @@ -86,4 +86,4 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
}
6 changes: 4 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<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.


<uses-feature
android:name="android.hardware.touchscreen"
Expand Down Expand Up @@ -65,7 +66,8 @@
</activity>
<activity android:name=".WidgetConfigActivity"
android:label="Widget Configuration"
android:theme="@style/Theme.WidgetConfigDialog" />
android:theme="@style/Theme.WidgetConfigDialog"
android:exported="true" />
Comment on lines 67 to +70
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.

<receiver android:name=".WidgetProvider"
android:exported="true">
<intent-filter>
Expand All @@ -80,4 +82,4 @@
</receiver>
</application>

</manifest>
</manifest>
118 changes: 107 additions & 11 deletions app/src/main/java/com/immichframe/immichframe/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ package com.immichframe.immichframe
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.annotation.SuppressLint
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.provider.Settings
import android.text.SpannableString
import android.text.Spanned
import android.text.style.RelativeSizeSpan
Expand Down Expand Up @@ -75,6 +79,7 @@ class MainActivity : AppCompatActivity() {
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.

private val imageRunnable = object : Runnable {
override fun run() {
if (isImageTimerRunning) {
Expand Down Expand Up @@ -488,6 +493,7 @@ class MainActivity : AppCompatActivity() {
attemptFetch()
}

// called when app starts and when user returns from settings screen
@SuppressLint("SetJavaScriptEnabled")
private fun loadSettings() {
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
Expand Down Expand Up @@ -607,6 +613,7 @@ class MainActivity : AppCompatActivity() {
}
)
}
applyScreenTimeout()
}

private fun onSettingsLoaded() {
Expand Down Expand Up @@ -770,11 +777,54 @@ class MainActivity : AppCompatActivity() {
}
}

// set screen timeout in Android settings in microseconds
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}")
}
Comment on lines +788 to +797
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.

}

// apply user-entered screen timeout value
private fun applyScreenTimeout() {
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)

if (keepScreenOn) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
val timeoutMinutes = prefs.getString("screenTimeout", "10")?.toIntOrNull() ?: 10
// capture original system timeout once if we haven't yet
if (originalScreenTimeout == -1) {
originalScreenTimeout = Settings.System.getInt(
contentResolver, Settings.System.SCREEN_OFF_TIMEOUT, 30000
)
}
setScreenTimeout(timeoutMinutes * 60 * 1000)
}
}

@Suppress("DEPRECATION")
private fun screenDim(dim: Boolean) {
val lp = window.attributes
if (dim == dimOverlay.isVisible) return

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)
Comment on lines 868 to +875
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.


// create black overlay, set webview to blank to reduce activity, and
// wait for screen to go to sleep after inactivity
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
if (dimOverlay.visibility != View.VISIBLE) {
dimOverlay.apply {
visibility = View.VISIBLE
Expand All @@ -791,19 +841,64 @@ class MainActivity : AppCompatActivity() {
.start()
}
}

// display message to user that screen is going to sleep
Toast.makeText(
this@MainActivity,
"Going to sleep",
Toast.LENGTH_LONG
).show()

} else {
lp.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
window.attributes = lp
// restore user's screen timeout value
if (originalScreenTimeout != -1) {
setScreenTimeout(originalScreenTimeout)
originalScreenTimeout = -1
}

// acquire WakeLock to turn screen on for short time, just to wake device
// FLAG_KEEP_SCREEN_ON will keep it on afterwards
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
val wakeLock = powerManager.newWakeLock(
PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
"ImmichFrame:WakeLockTag"
)
wakeLock.acquire(10 * 1000L) // 10 second timeout
wakeLock.release()

// remove black overlay and restore webview settings
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
if (dimOverlay.isVisible) {
dimOverlay.animate()
.alpha(0f)
.setDuration(500L)
.withEndAction {
dimOverlay.visibility = View.GONE
loadSettings()
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.

}
}

Expand Down Expand Up @@ -841,21 +936,22 @@ class MainActivity : AppCompatActivity() {
}
}

// called when user returns to app
override fun onResume() {
super.onResume()
if (keepScreenOn) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
applyScreenTimeout()
hideSystemUI()

}

// called when app is closed
override fun onDestroy() {
super.onDestroy()
rcpServer.stop()
handler.removeCallbacksAndMessages(null)
// restore timeout if we changed it
if (originalScreenTimeout != -1) {
setScreenTimeout(originalScreenTimeout)
}
}

private fun loadWebViewWithRetry(
Expand Down
44 changes: 39 additions & 5 deletions app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
Expand All @@ -22,11 +23,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
val chkUseWebView = findPreference<SwitchPreferenceCompat>("useWebView")
val chkBlurredBackground = findPreference<SwitchPreferenceCompat>("blurredBackground")
val chkShowCurrentDate = findPreference<SwitchPreferenceCompat>("showCurrentDate")
val chkKeepScreenOn = findPreference<SwitchPreferenceCompat>("keepScreenOn")
val txtScreenTimeout = findPreference<EditTextPreference>("screenTimeout")
val screenDimmingCategory = findPreference<PreferenceCategory>("screenDimmingCategory")
val chkScreenDim = findPreference<SwitchPreferenceCompat>("screenDim")
val txtDimTime = findPreference<EditTextPreference>("dim_time_range")


//obfuscate the authSecret
// obfuscate the authSecret
val authPref = findPreference<EditTextPreference>("authSecret")
authPref?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
Expand All @@ -36,22 +39,52 @@ class SettingsFragment : PreferenceFragmentCompat() {
val useWebView = chkUseWebView?.isChecked ?: false
chkBlurredBackground?.isVisible = !useWebView
chkShowCurrentDate?.isVisible = !useWebView

val keepScreenOn = chkKeepScreenOn?.isChecked ?: false
screenDimmingCategory?.isVisible = keepScreenOn
txtScreenTimeout?.isVisible = !keepScreenOn

val screenDim = chkScreenDim?.isChecked ?: false
txtDimTime?.isVisible = screenDim

// React to changes

// use Webview setting
chkUseWebView?.setOnPreferenceChangeListener { _, newValue ->
val value = newValue as Boolean
chkBlurredBackground?.isVisible = !value
chkShowCurrentDate?.isVisible = !value
//add android settings button
true
}

// keep screen on setting - toggles screen dimming category visibility
chkKeepScreenOn?.setOnPreferenceChangeListener { _, newValue ->
val value = newValue as Boolean
screenDimmingCategory?.isVisible = value
txtScreenTimeout?.isVisible = !value
true
}
Comment on lines +68 to +77
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.


// validate screen on timeout value
txtScreenTimeout?.setOnPreferenceChangeListener { _, newValue ->
val value = newValue.toString().toIntOrNull()
if (value != null && value > 0) {
true
} else {
Toast.makeText(requireContext(), "Please enter a valid number of minutes", Toast.LENGTH_SHORT).show()
false
}
}

// screen dimming setting - dimming time becomes visible if set
chkScreenDim?.setOnPreferenceChangeListener { _, newValue ->
val value = newValue as Boolean
txtDimTime?.isVisible = value
true
}

// settings lock setting: prevent further access to settings screen
val chkSettingsLock = findPreference<SwitchPreferenceCompat>("settingsLock")
chkSettingsLock?.setOnPreferenceChangeListener { _, newValue ->
val enabling = newValue as Boolean
Expand All @@ -72,7 +105,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
true
}


// close settings view
val btnClose = findPreference<Preference>("closeSettings")
btnClose?.setOnPreferenceClickListener {
val url = PreferenceManager.getDefaultSharedPreferences(requireContext())
Expand All @@ -88,6 +121,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}

// launch Android settings selection
val btnAndroidSettings = findPreference<Preference>("androidSettings")
btnAndroidSettings?.setOnPreferenceClickListener {
val context = requireContext()
Expand All @@ -111,7 +145,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
true
}


// get dimming time range setting from input string
val timePref = findPreference<EditTextPreference>("dim_time_range")
timePref?.setOnPreferenceChangeListener { _, newValue ->
val timeRange = newValue.toString().trim()
Expand All @@ -138,4 +172,4 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}
}
}
}
5 changes: 5 additions & 0 deletions app/src/main/res/xml/device_admin.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
<uses-policies>
<policy android:name="android.app.device_admin.DEVICE_ADMIN" />
</uses-policies>
</device-admin>
Loading