Skip to content

Active hours schedule with screen-off and power-button wake override#58

Open
patbaumgartner wants to merge 2 commits into
immichFrame:mainfrom
patbaumgartner:feature/active-schedule
Open

Active hours schedule with screen-off and power-button wake override#58
patbaumgartner wants to merge 2 commits into
immichFrame:mainfrom
patbaumgartner:feature/active-schedule

Conversation

@patbaumgartner

@patbaumgartner patbaumgartner commented Jun 28, 2026

Copy link
Copy Markdown

Summary

This PR adds an optional Active Times schedule that lets the photo frame turn its screen off and put the device to sleep during configured inactive hours, then wake itself back up at the next active period. It also adds a manual power-button override so you can briefly glance at the frame while it is asleep without disturbing the schedule.

Everything is opt-in: with the feature disabled (default) the frame behaves exactly as before.

Motivation

A wall-mounted frame usually only needs to be on during certain hours (e.g. daytime). Previously the screen stayed on 24/7 (or only dimmed). This lets the frame fully power its panel off overnight to save power / panel wear and reliably turn itself back on in the morning — while still letting you wake it on demand with the power button.

Features

1. Per-weekday active schedule

  • A Compose-based editor (ActiveScheduleActivity) lets you define rules, each with a set of weekdays and one or more time ranges (HH:mmHH:mm).
  • Overnight ranges are supported (e.g. 22:0006:00); the part before midnight is anchored to the day the range starts on.
  • The schedule is stored as JSON in shared preferences under activeSchedule.
  • An empty schedule means "always active", so enabling the feature before configuring any rule never blacks out the screen.

2. Screen-off during inactive hours

When the current time falls outside every active range, the frame:

  • stops the image and weather refresh timers and blanks the WebView,
  • dims to black and drops FLAG_KEEP_SCREEN_ON / FLAG_SHOW_WHEN_LOCKED,
  • and — if the screen-off permission (device admin) is granted — calls DevicePolicyManager.lockNow() to actually power the panel off and let the device sleep.

Without the device-admin permission the frame still dims to black; with it, the panel truly turns off.

3. Self-wake at the next active time

  • When going inactive, the frame schedules an AlarmManager RTC_WAKEUP alarm at the next active-start time using setExactAndAllowWhileIdle, which fires even from Doze (available since API 23 = minSdk).
  • ScheduleWakeReceiver receives the alarm and brings MainActivity to the foreground; the activity re-evaluates the schedule and powers the screen on.
  • On API 31+ where exact alarms can be denied, it falls back to an inexact set() wake.

4. Manual power-button override

This is the key on-demand feature:

  • While the schedule has the frame asleep, pressing the device power button turns the screen on. A dynamically-registered BroadcastReceiver listens for ACTION_SCREEN_ON and shows the frame temporarily.
  • The temporary view deliberately omits FLAG_KEEP_SCREEN_ON, so the device's normal screen timeout still applies — the user gets a quick look, and the screen powers off on its own.
  • When the screen turns off again (ACTION_SCREEN_OFF), the override ends, the schedule resumes, and the wake alarm is re-armed for the next rule.
  • The override never cancels the pending wake alarm and never flips the schedule state, so the regular schedule continues uninterrupted afterward.

Implementation notes

  • Helpers.ktActiveSchedule/ActiveRule/ActiveRange model, JSON (de)serialization, and the schedule math: isActiveNow() (handles same-day, overnight, and start == end all-day ranges) and nextActiveStart() (scans minute-by-minute up to 8 days ahead).
  • MainActivity.kt — tri-state isFrameInactive (null/true/false) to avoid redundant transitions; setFrameActive() to enter/leave the inactive state; the screen on/off receiver for the override; wakeScreen() (brief SCREEN_BRIGHT_WAKE_LOCK + ACQUIRE_CAUSES_WAKEUP); wake-alarm scheduling/cancelling; and lockDeviceIfPossible().
    • onResume() re-evaluates the schedule when active times are enabled. This is important: when the wake alarm brings an already-running instance forward via REORDER_TO_FRONT, onCreate() does not run, so re-checking in onResume() powers the screen on immediately instead of waiting for the next periodic (30 s) check.
  • ScheduleWakeReceiver.kt — broadcast receiver (not exported) that relaunches MainActivity.
  • FrameDeviceAdminReceiver.kt + res/xml/device_admin.xml — a minimal device-admin component using only the force-lock policy, used solely to turn the screen off.
  • SettingsFragment.kt — new Active Times preference category: an enable switch, a link to the schedule editor, and a screen-off-permission toggle that requests/clears device admin. Live summaries show the configured schedule and the permission state. Dialogs migrated to MaterialAlertDialogBuilder.
  • ActiveScheduleActivity.kt — the Jetpack Compose schedule editor (add/remove rules, pick weekdays with quick presets, add/edit/remove time ranges via the time picker).
  • res/xml/settings_view.xml, res/values/strings.xml, res/values/styles.xml, res/layout/pref_close_button.xml — settings UI, strings, theming (purple accent on controls), and a styled close button.
  • AndroidManifest.xml — registers ActiveScheduleActivity, the FrameDeviceAdminReceiver (with BIND_DEVICE_ADMIN), and ScheduleWakeReceiver (with the custom wake action).

Permissions

  • Device admin (force-lock) — optional, user-granted from Settings. Only used to call lockNow() to power the screen off. If not granted, the frame falls back to dimming to black.
  • No new dangerous runtime permissions are added.

Compatibility

  • minSdk 23 is respected: setExactAndAllowWhileIdle is API 23; setShowWhenLocked/setTurnScreenOn/requestDismissKeyguard are guarded behind O_MR1 with the legacy window flags as the baseline.

Testing

Validated on a physical device (Android 6.0.1) with a structured 5-cycle harness exercising the full contract per cycle:

Phase Scenario Expected
B Cold start inside an inactive window Screen OFF + wake alarm armed
C Power-button override (WAKEUP) Screen ON, KEEP_SCREEN_ON absent, SHOW_WHEN_LOCKED set, alarm not cancelled
D Screen timeout (SLEEP) → schedule resumes Screen OFF + alarm re-armed
E Real scheduled alarm fires (live process) Screen ON, Awake, KEEP_SCREEN_ON set, stays on

Result: 85 / 85 assertions passed across 5 cycles. Window flags were verified via dumpsys window windows, display/wakefulness via dumpsys power, and alarm arming via dumpsys alarm.

Notes for reviewers

  • The feature is fully behind the Set Active Hours switch; default behavior is unchanged.
  • The override intentionally does not hold the screen on — that is what makes it a quick "peek" rather than a state change.

Summary by CodeRabbit

  • New Features

    • Added “Active Times” scheduling so the app can automatically stay active or sleep during selected hours and days.
    • Added controls to edit schedules, enable device-admin behavior, and wake the app at the right time.
    • Added a more prominent custom “Close Settings” button.
  • Bug Fixes

    • Improved schedule handling so invalid or incomplete rules are ignored and saved schedules stay consistent.
    • Updated settings summaries to better reflect the current schedule and admin status.

…erride

Add an optional "Active Times" schedule that lets the frame turn its screen
off and sleep the device during configured inactive hours, then wake itself
back up at the next active period. Also add a manual power-button override so
the user can briefly view the frame while it is asleep.

Features
- Per-weekday active schedule (multiple rules, multiple ranges per rule,
  overnight ranges supported) edited in a Compose-based editor.
- During inactive hours the frame stops refreshing, dims to black, drops the
  keep-screen-on flags and (with device-admin permission) calls lockNow() to
  actually power the screen off and let the device sleep.
- An AlarmManager RTC_WAKEUP alarm (setExactAndAllowWhileIdle, wakes from Doze)
  brings MainActivity to the foreground at the next active-start time and the
  screen powers back on.
- Manual power-button override: pressing power while asleep shows the frame
  temporarily without FLAG_KEEP_SCREEN_ON, so the normal screen timeout still
  applies; when the screen turns off the schedule resumes and the wake alarm is
  re-armed.

Implementation
- Helpers: ActiveSchedule model + JSON (de)serialization, isActiveNow() and
  nextActiveStart() (empty schedule == always active so the screen never blacks
  out unconfigured).
- MainActivity: tri-state isFrameInactive, setFrameActive(), screen on/off
  BroadcastReceiver for the override, wake alarm scheduling, lockDeviceIfPossible(),
  and onResume re-evaluation so an already-running instance woken via
  REORDER_TO_FRONT powers the screen on immediately.
- ScheduleWakeReceiver: broadcast receiver that relaunches MainActivity.
- FrameDeviceAdminReceiver + device_admin.xml (force-lock policy) for screen-off.
- SettingsFragment: Active Times category (enable switch, schedule editor,
  screen-off permission toggle) with live summaries; Material dialogs.
- ActiveScheduleActivity: Compose schedule editor.
- Manifest: register the two receivers and the editor activity.
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

More reviews will be available in 48 minutes and 1 second. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

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

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fe655c3b-00a7-42d0-9350-c5421542bfba

📥 Commits

Reviewing files that changed from the base of the PR and between 200247e and e272c81.

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

Walkthrough

Adds a configurable active-hours scheduling system to the Android frame app. New data models, parse/serialize/evaluate helpers, and a Compose schedule editor let users define per-weekday time rules. MainActivity gains screen-state receivers, setFrameActive/checkActiveTime logic, device-admin locking, and AlarmManager-based wake scheduling. SettingsFragment is wired to the new editor and device admin flow.

Changes

Active Schedule & Device Admin

Layer / File(s) Summary
Schedule data models and helpers
app/src/main/java/com/immichframe/immichframe/Helpers.kt
Adds ActiveRange, ActiveRule, ActiveSchedule data classes and parseActiveSchedule, serializeActiveSchedule, isActiveNow, nextActiveStart functions including overnight-range and empty-schedule handling.
DeviceAdmin and WakeAlarm receivers
app/src/main/java/com/immichframe/immichframe/FrameDeviceAdminReceiver.kt, app/src/main/java/com/immichframe/immichframe/ScheduleWakeReceiver.kt, app/src/main/res/xml/device_admin.xml, app/src/main/AndroidManifest.xml
Adds FrameDeviceAdminReceiver (with componentName helper), ScheduleWakeReceiver (relaunches MainActivity on alarm), device_admin.xml with force-lock policy, and both receivers' manifest declarations.
MainActivity active-schedule power management
app/src/main/java/com/immichframe/immichframe/MainActivity.kt
Registers a screen-state BroadcastReceiver; adds checkActiveTime/setFrameActive to control window flags, dim overlay, timers, and keyguard; adds scheduleWakeAlarm/cancelWakeAlarm using AlarmManager; adds manual-override and onResume/onDestroy lifecycle hooks.
Compose schedule editor
app/src/main/java/com/immichframe/immichframe/ActiveScheduleActivity.kt, app/src/main/AndroidManifest.xml
New ActiveScheduleActivity with ScheduleEditorScreen and RuleCard composables for weekday chip selection, time-range management via TimePickerDialog, and save/validation before serializing back to shared preferences.
Settings UI wiring and summaries
app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt, app/src/main/res/xml/settings_view.xml, app/src/main/res/layout/pref_close_button.xml, app/src/main/res/values/strings.xml, app/src/main/res/values/styles.xml
Wires activeTimes switch visibility, edit/admin click handlers, human-readable schedule summary generation, and device admin enable/disable dialog; updates settings XML with new "Active Times" preference category, custom close-button layout, and new string/style resources.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SettingsFragment
  participant ActiveScheduleActivity
  participant Helpers
  participant MainActivity
  participant AlarmManager
  participant DevicePolicyManager

  User->>SettingsFragment: enable activeTimes switch
  SettingsFragment->>MainActivity: loadSettings() triggers activeCheckRunnable
  MainActivity->>Helpers: isActiveNow(schedule, now)
  alt schedule says inactive
    MainActivity->>DevicePolicyManager: lockNow()
    MainActivity->>Helpers: nextActiveStart(schedule, now)
    MainActivity->>AlarmManager: scheduleWakeAlarm(nextStart)
  end
  AlarmManager->>MainActivity: ACTION_SCHEDULE_WAKE fires via ScheduleWakeReceiver
  MainActivity->>Helpers: isActiveNow(schedule, now)
  MainActivity->>MainActivity: setFrameActive(true): wake screen, load settings
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hippity-hop through the night,
The frame knows when to dim its light.
Wake alarms tick, the schedule hums,
Lock the screen 'til morning comes.
With admin rights and timely dreams,
ImmichFrame sleeps — or so it seems! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature: active scheduling with screen-off behavior and power-button override.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/com/immichframe/immichframe/ActiveScheduleActivity.kt`:
- Around line 108-119: The save logic in ActiveScheduleActivity’s onClick
handler is silently filtering out invalid rules and allowing zero-length ranges,
which can turn a mistaken schedule into always-active behavior. Update the
validation before calling onSave(Helpers.ActiveSchedule(...)) to explicitly
reject rules with empty days, missing ranges, or any range where start equals
end instead of dropping them. Surface an error/validation message to the user
and only proceed when every rule in the rules list is complete and all
ActiveRange values are valid.

In `@app/src/main/java/com/immichframe/immichframe/MainActivity.kt`:
- Around line 877-887: The alarm resync logic in checkActiveTime is skipping
setFrameActive(false) when isFrameInactive is already true, which leaves
AlarmManager stuck on an old wake time after schedule changes. Update the
active-time handling so schedule changes always refresh the wake alarm even
while inactive/manual-override is set, and make sure a null next time clears any
previously scheduled alarm instead of keeping it. Apply the same fix to the
related logic around setFrameActive and any duplicate path near the other
referenced block.
- Around line 962-968: The on-screen-off flow in onScreenTurnedOff() always
clears manual override and calls setFrameActive(false), which can ignore the
current Active Times state or a schedule that has already become active. Update
this method to re-evaluate the schedule/state before ending isManualOverride,
and only deactivate the frame if the current conditions still require it. Use
the existing onScreenTurnedOff, isManualOverride, and setFrameActive logic to
keep the frame aligned with the latest schedule.

In `@app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt`:
- Around line 67-72: Turning off Active Times currently only hides the nested
preferences in the chkActiveTimes change listener, so the existing wake
alarm/inactive frame state remains in effect. Update the Active Times off
transition in SettingsFragment’s preference change handling to explicitly clear
schedule side effects: reactivate the frame, cancel any pending schedule
enforcement/wake alarm, or trigger the corresponding cleanup path in
MainActivity. Make sure the new behavior is tied to the chkActiveTimes toggle
and is consistent with what MainActivity.onResume expects when activeTimes
becomes false.
🪄 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: c4f3f1ee-f7ed-4bee-b59d-25b20e3041c1

📥 Commits

Reviewing files that changed from the base of the PR and between e40d7a1 and 200247e.

📒 Files selected for processing (12)
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/immichframe/immichframe/ActiveScheduleActivity.kt
  • app/src/main/java/com/immichframe/immichframe/FrameDeviceAdminReceiver.kt
  • app/src/main/java/com/immichframe/immichframe/Helpers.kt
  • app/src/main/java/com/immichframe/immichframe/MainActivity.kt
  • app/src/main/java/com/immichframe/immichframe/ScheduleWakeReceiver.kt
  • app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt
  • app/src/main/res/layout/pref_close_button.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/values/styles.xml
  • app/src/main/res/xml/device_admin.xml
  • app/src/main/res/xml/settings_view.xml

Comment thread app/src/main/java/com/immichframe/immichframe/MainActivity.kt
Comment thread app/src/main/java/com/immichframe/immichframe/MainActivity.kt
Comment thread app/src/main/java/com/immichframe/immichframe/SettingsFragment.kt
- ActiveScheduleActivity: validate rules before saving; reject incomplete
  rules (no days / no ranges), malformed times, and zero-length ranges via a
  Toast instead of silently dropping them. Empty schedule still means
  always-active.
- MainActivity.checkActiveTime: re-arm the wake alarm when already inactive so
  a changed schedule no longer leaves a stale wake time.
- MainActivity.scheduleWakeAlarm: cancel any prior alarm before computing the
  next start so an empty/expired schedule clears the pending wake.
- MainActivity.onScreenTurnedOff: re-evaluate the schedule (and the
  activeTimes toggle) before ending the manual override instead of always
  sleeping.
- MainActivity.onResume: when Active Times is disabled while the frame is
  asleep, cancel the wake alarm and reactivate the frame so disabling the
  feature fully clears its side effects.
@patbaumgartner

Copy link
Copy Markdown
Author

I’m pleasantly surprised by how well the new feature works on the Denver Frameo PFF-1042DWMK3. It wakes up at the scheduled times and goes back to sleep as expected.

@3rob3

3rob3 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Sorry, just saw this. Let me have a look, and thanks for contributing! Some quick comments:

  • I have a feeling Google will reject this due to the Device Admin, even though it is optional. They have been very strict about this kind of thing. I guess we can always try.
  • Not sure I agree on the power button functionality. I am having a hard time imagining wanting to "peek" at a photo frame. I could see wanting that on a Home Assistant dashboard. My natural instinct of this function would be it would "break" the sleep schedule and re-awake it even though you are outside of the wake hours. For instance you wake up earlier than normal, frame is off, and want to quickly turn it back on until the next sleep time.

@patbaumgartner

Copy link
Copy Markdown
Author

Thanks for taking a look!

Device Admin: Fair point, Google has been strict about BIND_DEVICE_ADMIN. A few things that might help here: it is fully optional (the schedule works without it, the frame just dims instead of actually locking the screen), it only requests the force-lock policy, and the user gets an explanation before enabling it. If Play review rejects it, I can keep the Device Admin path only for sideloaded installs on dedicated frame devices (the main use case anyway, e.g. Frameo hardware) or drop it and rely on the dim overlay alone. Happy to go whichever direction you prefer.

Power button behavior: It actually already works the way you describe, so we agree :) Pressing the power button while the frame is asleep is not just a quick peek. It sets a manual override that suppresses the schedule, so the frame wakes up and stays on until the screen turns off again or the next sleep transition kicks in. The motivation was exactly your scenario, plus one more: being able to open the app settings (or develop against the device) while it is inside its sleep window, without the schedule immediately forcing it back to sleep. I will clarify the PR description and code comments so "peek" does not suggest a temporary flash.

@3rob3

3rob3 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Perfect! Yeah i think the word peek through me off. I haven't reviewed code yet either which would have likely cleared up the intent.

@patbaumgartner

Copy link
Copy Markdown
Author

I actually also fixed the coloring on the settings page and improved the close button 🙂

Take your time for the review. It's running on my device 😉

@3rob3

3rob3 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

This looks REALLY great. I feel like Active Hours should replace the Screen Dimming, since this might confuse people and they both serve the same purpose. What do you think?

@patbaumgartner

Copy link
Copy Markdown
Author

I agree. But wanted to keep it backwards compatible, and not breaking any installations. Do you want me to remove the old dimming feature?

@3rob3

3rob3 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Ya, I think so. They do the same thing, and your pickers make it real easy to recreate whatever they were using. Plus the additional true screen off function makes up for any inconvenience :).

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