-
Notifications
You must be signed in to change notification settings - Fork 12
feat: screen turn-off #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
51016f0
1f65e55
3c6d2a3
d12f17f
2d83d95
0be4a56
0d11e2d
42e8d21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" /> | ||
|
|
||
| <uses-feature | ||
| android:name="android.hardware.touchscreen" | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.ktRepository: immichFrame/ImmichFrame_Android Length of output: 1048 🏁 Script executed: cat app/src/main/java/com/immichframe/immichframe/WidgetConfigActivity.ktRepository: immichFrame/ImmichFrame_Android Length of output: 2214 Reject invalid appWidgetId before accessing preferences.
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
setResult(RESULT_CANCELED)
finish()
return
}🤖 Prompt for AI Agents |
||
| <receiver android:name=".WidgetProvider" | ||
| android:exported="true"> | ||
| <intent-filter> | ||
|
|
@@ -80,4 +82,4 @@ | |
| </receiver> | ||
| </application> | ||
|
|
||
| </manifest> | ||
| </manifest> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -75,6 +79,8 @@ 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make timeout backup lifecycle-safe and idempotent. Line 82 keeps backup only in memory, and Lines 795-797 can overwrite it on repeated 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 |
||
| private var isSnoozing = false | ||
| private val imageRunnable = object : Runnable { | ||
| override fun run() { | ||
| if (isImageTimerRunning) { | ||
|
|
@@ -97,6 +103,12 @@ class MainActivity : AppCompatActivity() { | |
| handler.postDelayed(this, 30000) | ||
| } | ||
| } | ||
| private val redimRunnable = object : Runnable { | ||
| override fun run() { | ||
| isSnoozing = false | ||
| screenDim(true) | ||
| } | ||
| } | ||
| private var isShowingFirst = true | ||
| private var zoomAnimator: ObjectAnimator? = null | ||
|
|
||
|
|
@@ -488,6 +500,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) | ||
|
|
@@ -518,7 +531,7 @@ class MainActivity : AppCompatActivity() { | |
| handler.post(dimCheckRunnable) | ||
| } else { | ||
| handler.removeCallbacks(dimCheckRunnable) | ||
| dimOverlay.visibility = View.GONE | ||
| removeDimOverlay() | ||
| val lp = WindowManager.LayoutParams() | ||
| lp.copyFrom(window.attributes) | ||
| lp.screenBrightness = 1f | ||
|
|
@@ -607,6 +620,7 @@ class MainActivity : AppCompatActivity() { | |
| } | ||
| ) | ||
| } | ||
| applyScreenTimeout() | ||
| } | ||
|
|
||
| private fun onSettingsLoaded() { | ||
|
|
@@ -770,39 +784,137 @@ class MainActivity : AppCompatActivity() { | |
| } | ||
| } | ||
|
|
||
| private fun screenDim(dim: Boolean) { | ||
| val lp = window.attributes | ||
| if (dim) { | ||
| lp.screenBrightness = 0.01f | ||
| window.attributes = lp | ||
| if (dimOverlay.visibility != View.VISIBLE) { | ||
| dimOverlay.apply { | ||
| visibility = View.VISIBLE | ||
| alpha = 0f | ||
| if (useWebView) { | ||
| webView.loadUrl("about:blank") | ||
| } else { | ||
| stopImageTimer() | ||
| stopWeatherTimer() | ||
| // 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.xmlRepository: immichFrame/ImmichFrame_Android Length of output: 953 🌐 Web query:
💡 Result:
What the official docs say:
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: Citations:
Guard The WRITE_SETTINGS permission requires explicit user grant through device Settings on Android 6.0+. Without checking 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 |
||
| } | ||
|
|
||
| // 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) | ||
| } | ||
| } | ||
|
|
||
| // show the black overlay and pause activity | ||
| private fun applyDimOverlay() { | ||
| if (dimOverlay.visibility != View.VISIBLE || dimOverlay.alpha < 0.5f) { | ||
| dimOverlay.apply { | ||
| visibility = View.VISIBLE | ||
| alpha = 0f | ||
| if (useWebView) { | ||
| webView.loadUrl("about:blank") | ||
| } else { | ||
| stopImageTimer() | ||
| stopWeatherTimer() | ||
| } | ||
| animate() | ||
| .alpha(0.99f) | ||
| .setDuration(500L) | ||
| .start() | ||
| } | ||
|
|
||
| // display message to user that screen is going to sleep | ||
| Toast.makeText( | ||
| this@MainActivity, | ||
| "Going to sleep", | ||
| Toast.LENGTH_LONG | ||
| ).show() | ||
| } | ||
| } | ||
|
|
||
| // hide the black overlay and resume activity | ||
| private fun removeDimOverlay() { | ||
| // remove black overlay and restore webview settings | ||
| handler.removeCallbacks(redimRunnable) | ||
| if (dimOverlay.isVisible) { | ||
| dimOverlay.animate() | ||
| .alpha(0f) | ||
| .setDuration(500L) | ||
| .withEndAction { | ||
| dimOverlay.visibility = View.GONE | ||
| loadSettings() // Restores WebView and timers | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { | ||
| setShowWhenLocked(false) | ||
| } | ||
| animate() | ||
| .alpha(0.99f) | ||
| .setDuration(500L) | ||
| .start() | ||
| } | ||
| .start() | ||
| } | ||
| } | ||
|
|
||
| @Suppress("DEPRECATION") | ||
| private fun screenDim(dim: Boolean) { | ||
| if (dim) { | ||
| // 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# First, let's find the MainActivity.kt file
find . -name "MainActivity.kt" -type fRepository: 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.ktRepository: 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.ktRepository: 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.ktRepository: 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 2Repository: 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 2Repository: 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 2Repository: immichFrame/ImmichFrame_Android Length of output: 1369 Replace Handler-based scheduling with AlarmManager for reliable undim triggering. The dim-to-undim flow relies on While the 🤖 Prompt for AI Agents |
||
|
|
||
| // 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) | ||
| applyDimOverlay() | ||
|
|
||
| } else { | ||
| lp.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE | ||
| window.attributes = lp | ||
| if (dimOverlay.isVisible) { | ||
| dimOverlay.animate() | ||
| .alpha(0f) | ||
| .setDuration(500L) | ||
| .withEndAction { | ||
| dimOverlay.visibility = View.GONE | ||
| loadSettings() | ||
| } | ||
| .start() | ||
| // 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() | ||
|
|
||
| window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) | ||
| removeDimOverlay() | ||
|
|
||
| // 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) | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -827,9 +939,11 @@ class MainActivity : AppCompatActivity() { | |
| } | ||
|
|
||
| val isOverlayVisible = dimOverlay.isVisible | ||
| if (shouldDim && !isOverlayVisible) { | ||
| if (shouldDim && !isOverlayVisible && !isSnoozing) { | ||
| isSnoozing = false | ||
| screenDim(true) | ||
| } else if (!shouldDim && isOverlayVisible) { | ||
| isSnoozing = false | ||
| screenDim(false) | ||
| } | ||
| } | ||
|
|
@@ -841,21 +955,33 @@ class MainActivity : AppCompatActivity() { | |
| } | ||
| } | ||
|
|
||
| // called when user returns to app | ||
| override fun onResume() { | ||
| super.onResume() | ||
| if (keepScreenOn) { | ||
| window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) | ||
|
|
||
| // if we are already in dim state, but screen just woke up, | ||
| // hide overlay and schedule screen to re-dim in 30 seconds | ||
| if (dimOverlay.isVisible) { | ||
| isSnoozing = true | ||
| setScreenTimeout(40000) | ||
| removeDimOverlay() | ||
| handler.postDelayed(redimRunnable, 30000) | ||
| } 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( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: immichFrame/ImmichFrame_Android
Length of output: 145
🏁 Script executed:
Repository: immichFrame/ImmichFrame_Android
Length of output: 218
🏁 Script executed:
Repository: immichFrame/ImmichFrame_Android
Length of output: 124
🏁 Script executed:
cat -n app/build.gradle.kts | head -100Repository: immichFrame/ImmichFrame_Android
Length of output: 3658
🏁 Script executed:
Repository: immichFrame/ImmichFrame_Android
Length of output: 4149
🏁 Script executed:
rg "WRITE_SETTINGS" -B 3 -A 3Repository: immichFrame/ImmichFrame_Android
Length of output: 663
🏁 Script executed:
rg "WRITE_SETTINGS\|Settings\.ACTION_MANAGE_WRITE_SETTINGS" app/src -B 2 -A 2Repository: immichFrame/ImmichFrame_Android
Length of output: 57
🏁 Script executed:
grep -r "flavors\|productFlavors\|flavorDimensions" app/build.gradle.ktsRepository: immichFrame/ImmichFrame_Android
Length of output: 57
🏁 Script executed:
Repository: immichFrame/ImmichFrame_Android
Length of output: 456
🏁 Script executed:
Repository: immichFrame/ImmichFrame_Android
Length of output: 494
🏁 Script executed:
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 3Repository: immichFrame/ImmichFrame_Android
Length of output: 101
🏁 Script executed:
rg "ACTION_MANAGE_WRITE_SETTINGS|canWrite|WRITE_SETTINGS" app/src/main/javaRepository: immichFrame/ImmichFrame_Android
Length of output: 57
🏁 Script executed:
Repository: immichFrame/ImmichFrame_Android
Length of output: 11002
🏁 Script executed:
Repository: immichFrame/ImmichFrame_Android
Length of output: 23994
Remove unused
WRITE_SETTINGSpermission.WRITE_SETTINGSis declared in the manifest but not used anywhere in the codebase. Screen timeout management is handled via app-levelWindowManager.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