feat(android): switch to screen-on rotation service and updated scheduler

The rotation service now responds to screen on/off events via a
dedicated ScreenOnRotationService, reducing background work and
avoiding perpetual wakeful states. Manifest and receiver logic were
adjusted to align with the new behavior and energy constraints.

Scheduling and enablement now consistently use timed-based rules and
screen-on preferences, with the boot and alarm paths updated to honor
these settings. This avoids unnecessary foreground services and keeps
the app compliant with Doze and battery optimizations.

Immich image handling improvements include throttled progress updates,
larger cache allowances, and a connectivity-aware automatic download
check to avoid unwanted network usage.

- Use ScreenOnRotationService for screen-on based rotation scheduling
- Centralize enablement via timed-enabled paths and AlarmManager windowing
- Improve Immich cache and progress reporting with connectivity checks
This commit is contained in:
2026-08-22 20:04:42 +02:00
parent 73f8599c7f
commit 75c79a8344
26 changed files with 245 additions and 161 deletions
+7 -8
View File
@@ -14,9 +14,10 @@ function SettingRow({ icon, label, value, onChange }: { icon: React.ReactNode; l
return <div className="setting-row"><div className="setting-icon">{icon}</div><span>{label}</span><Switch checked={value} onChange={onChange} label={label} /></div>;
}
function IntervalRow({ value, onChange, label, paused, everyMinutes }: { value: number; onChange: (value: number) => void; label: string; paused: string; everyMinutes: (minutes: number) => string }) {
function IntervalRow({ value, onChange, label, paused, screenOn, everyMinutes }: { value: number; onChange: (value: number) => void; label: string; paused: string; screenOn: string; everyMinutes: (minutes: number) => string }) {
return <label className="setting-row interval-row"><div className="setting-icon"><Smartphone /></div><span>{label}</span><select value={value} onChange={event => onChange(Number(event.target.value))} aria-label={label}>
<option value={0}>{paused}</option>
<option value={-1}>{screenOn}</option>
{[5, 15, 30, 60, 180, 360, 720].map(minutes => <option value={minutes} key={minutes}>{everyMinutes(minutes)}</option>)}
</select></label>;
}
@@ -54,11 +55,9 @@ export default function App() {
const refresh = () => { void getState().then(setState).catch(() => undefined); };
refresh();
const onVisibilityChange = () => { if (!document.hidden) refresh(); };
const refreshInterval = window.setInterval(() => { if (!document.hidden) refresh(); }, 15000);
document.addEventListener("visibilitychange", onVisibilityChange);
window.addEventListener("focus", refresh);
return () => {
window.clearInterval(refreshInterval);
document.removeEventListener("visibilitychange", onVisibilityChange);
window.removeEventListener("focus", refresh);
};
@@ -118,11 +117,11 @@ export default function App() {
}
async function updateInterval(minutes: number) {
setState(prev => ({ ...prev, intervalMinutes: minutes, enabled: minutes > 0 }));
setState(prev => ({ ...prev, intervalMinutes: minutes, enabled: minutes !== 0 }));
try {
const saved = await setIntervalMinutes(minutes);
setState(saved);
setNotice(minutes > 0 ? t.intervalSaved : t.automaticDisabled);
setNotice(minutes !== 0 ? t.intervalSaved : t.automaticDisabled);
}
catch { setNotice(t.androidOnly); }
}
@@ -249,7 +248,7 @@ export default function App() {
setNotice(it.importing);
const poll = window.setInterval(() => {
void getImmichImportProgress().then(setImmichImportProgress).catch(() => undefined);
}, 250);
}, 750);
try {
setState(await importImmichAssets([...immichSelected]));
setImmichSelected(new Set());
@@ -408,7 +407,7 @@ export default function App() {
<div className="hero-meta"><span><Sparkles size={16} /> {t.currentImage}</span><button onClick={next} disabled={busy}>{t.nextImage} <ChevronRight size={18} /></button></div>
</section>
<IntervalRow value={state.intervalMinutes} onChange={updateInterval} label={t.changeInterval} paused={t.paused} everyMinutes={t.everyMinutes} />
<IntervalRow value={state.intervalMinutes} onChange={updateInterval} label={t.changeInterval} paused={t.paused} screenOn={t.screenOn} everyMinutes={t.everyMinutes} />
<button className="primary" onClick={choose} disabled={busy}><Images />{busy ? t.pleaseWait : t.selectImages}</button>
@@ -421,7 +420,7 @@ export default function App() {
</> : tab === "settings" ? <section className="settings-page">
<h2>{t.settings}</h2><p>{t.settingsIntro}</p>
<label className="language-setting"><div className="setting-icon"><Languages /></div><div><strong>{t.language}</strong><span>{languageNames[language]}</span></div><select className="locale-select" value={language} onChange={event => setLanguage(event.target.value as Language)} aria-label={t.language}>{languages.map(locale => <option value={locale} key={locale}>{languageNames[locale]}</option>)}</select></label>
<div className="settings-list"><IntervalRow value={state.intervalMinutes} onChange={updateInterval} label={t.changeInterval} paused={t.paused} everyMinutes={t.everyMinutes} /><SettingRow icon={<Shuffle />} label={t.shuffle} value={state.shuffle} onChange={v => update("shuffle", v)} /><SettingRow icon={<LockKeyhole />} label={t.lockScreenOnly} value={state.lockScreenOnly} onChange={v => update("lockScreenOnly", v)} /></div>
<div className="settings-list"><IntervalRow value={state.intervalMinutes} onChange={updateInterval} label={t.changeInterval} paused={t.paused} screenOn={t.screenOn} everyMinutes={t.everyMinutes} /><SettingRow icon={<Shuffle />} label={t.shuffle} value={state.shuffle} onChange={v => update("shuffle", v)} /><SettingRow icon={<LockKeyhole />} label={t.lockScreenOnly} value={state.lockScreenOnly} onChange={v => update("lockScreenOnly", v)} /></div>
<section className="immich-settings">
<div className="immich-title"><span><Cloud /></span><div><h3>{it.immich}</h3><p>{it.immichIntro}</p></div></div>
{immichConnection.configured ? <>