diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4f7d508..d206dd6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,7 +32,7 @@ android { minSdk = 23 targetSdk = 36 versionCode = 48 - versionName = "1.0.48.0" + versionName = "1.0.48.2" } buildTypes { @@ -86,4 +86,4 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) -} \ No newline at end of file +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 06e8c15..8c2eba2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + + android:theme="@style/Theme.WidgetConfigDialog" + android:exported="true" /> @@ -80,4 +82,4 @@ - \ No newline at end of file + diff --git a/app/src/main/java/com/immichframe/immichframe/MainActivity.kt b/app/src/main/java/com/immichframe/immichframe/MainActivity.kt index ed2b99b..8a9335f 100644 --- a/app/src/main/java/com/immichframe/immichframe/MainActivity.kt +++ b/app/src/main/java/com/immichframe/immichframe/MainActivity.kt @@ -3,6 +3,8 @@ 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 @@ -10,6 +12,8 @@ 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 + 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}") + } + } + + // 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) + + // 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( diff --git a/app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt b/app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt index d2f52ad..6698fed 100644 --- a/app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt +++ b/app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt @@ -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 @@ -22,11 +23,13 @@ class SettingsFragment : PreferenceFragmentCompat() { val chkUseWebView = findPreference("useWebView") val chkBlurredBackground = findPreference("blurredBackground") val chkShowCurrentDate = findPreference("showCurrentDate") + val chkKeepScreenOn = findPreference("keepScreenOn") + val txtScreenTimeout = findPreference("screenTimeout") + val screenDimmingCategory = findPreference("screenDimmingCategory") val chkScreenDim = findPreference("screenDim") val txtDimTime = findPreference("dim_time_range") - - //obfuscate the authSecret + // obfuscate the authSecret val authPref = findPreference("authSecret") authPref?.setOnBindEditTextListener { editText -> editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD @@ -36,10 +39,21 @@ 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 + if (!keepScreenOn) { + chkScreenDim?.isChecked = false + txtDimTime?.isVisible = false + } + 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 @@ -47,11 +61,40 @@ class SettingsFragment : PreferenceFragmentCompat() { //add android settings button true } + + // keep screen on setting - toggles screen dimming category visibility + // show screen dimming only if keep-on is set + // show screen timeout setting only if keep-on is not set + chkKeepScreenOn?.setOnPreferenceChangeListener { _, newValue -> + val value = newValue as Boolean + screenDimmingCategory?.isVisible = value + txtScreenTimeout?.isVisible = !value + if (!value) { + chkScreenDim?.isChecked = false + txtDimTime?.isVisible = false + } + true + } + + // validate screen on timeout value + txtScreenTimeout?.setOnPreferenceChangeListener { _, newValue -> + val value = newValue.toString().toIntOrNull() + if (value != null && value in 1..1440) { + true + } else { + Toast.makeText(requireContext(), "Please enter a value between 1 and 1440 minutes (24 hours)", 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("settingsLock") chkSettingsLock?.setOnPreferenceChangeListener { _, newValue -> val enabling = newValue as Boolean @@ -72,7 +115,7 @@ class SettingsFragment : PreferenceFragmentCompat() { true } - + // close settings view val btnClose = findPreference("closeSettings") btnClose?.setOnPreferenceClickListener { val url = PreferenceManager.getDefaultSharedPreferences(requireContext()) @@ -88,6 +131,7 @@ class SettingsFragment : PreferenceFragmentCompat() { } } + // launch Android settings selection val btnAndroidSettings = findPreference("androidSettings") btnAndroidSettings?.setOnPreferenceClickListener { val context = requireContext() @@ -111,9 +155,8 @@ class SettingsFragment : PreferenceFragmentCompat() { true } - - val timePref = findPreference("dim_time_range") - timePref?.setOnPreferenceChangeListener { _, newValue -> + // get dimming time range setting from input string + txtDimTime?.setOnPreferenceChangeListener { _, newValue -> val timeRange = newValue.toString().trim() val regex = "^([01]?[0-9]|2[0-3]):([0-5][0-9])-([01]?[0-9]|2[0-3]):([0-5][0-9])$".toRegex() @@ -138,4 +181,4 @@ class SettingsFragment : PreferenceFragmentCompat() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/res/xml/device_admin.xml b/app/src/main/res/xml/device_admin.xml new file mode 100644 index 0000000..17e5710 --- /dev/null +++ b/app/src/main/res/xml/device_admin.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/xml/settings_view.xml b/app/src/main/res/xml/settings_view.xml index 75112b5..63c838b 100644 --- a/app/src/main/res/xml/settings_view.xml +++ b/app/src/main/res/xml/settings_view.xml @@ -16,36 +16,44 @@ + android:title="Use WebView" /> + android:title="Keep Screen On" /> + + android:title="Blurred Background" /> + android:title="Show Current Date" /> + android:title="Lock access to settings" /> - + + android:title="Set Screen Dimming Hours" /> + android:summary="e.g. 22:00-07:00" + android:title="Dimming Time Range" /> diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/settings.gradle.kts b/settings.gradle.kts index 2c9f205..205baf6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,9 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories {