Skip to content

Commit 3c48bcf

Browse files
jplexerclaude
andcommitted
fw/shell: halt the app idle timeout while a finger is on the screen
A motionless hold in the launcher (or any app under the idle timeout) emits no touch events after Touchdown -- the touch service only publishes PositionUpdate when the coordinates change -- so nothing could refresh the 30 s watchface timeout and it fired mid-touch, yanking the user back to the watchface while their finger was still down. Track the physical finger in app_idle_timeout as its own inhibitor, driven from the kernel touch-event handler: Touchdown halts the timer, Liftoff restarts it from a full interval. A separate flag (rather than reusing pause/resume) keeps the hold composed correctly with the focus-driven pause, so a modal push/pop mid-touch can neither resurrect nor permanently kill the timeout. The disable/withdraw paths already synthesize a Liftoff for a finger that is still down, so the hold cannot leak. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Joshua Jun <lets@throw.rocks>
1 parent 75733b1 commit 3c48bcf

7 files changed

Lines changed: 169 additions & 1 deletion

File tree

src/fw/kernel/event_loop.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,13 @@ static NOINLINE void prv_minimal_event_handler(PebbleEvent* e) {
335335
PBL_ANALYTICS_ADD(touch_gated_touchdown_count, 1);
336336
}
337337
touch_session_extend();
338+
// A finger on the screen is ongoing interaction: halt the app idle timeout until liftoff.
339+
// A motionless hold emits no further touch events, so a timer refresh alone can't cover it.
340+
app_idle_timeout_touch_down();
338341
} else if (e->touch.event.type == TouchEvent_Liftoff) {
339342
light_touch_up();
340343
touch_session_extend();
344+
app_idle_timeout_touch_up();
341345
}
342346
if (compositor_is_animating() || is_modal_focused) {
343347
// Mask the app task while the compositor animates or a modal is focused. Otherwise a

src/fw/shell/normal/app_idle_timeout.c

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
TimerID s_timer;
1616
bool s_app_paused = false;
1717
bool s_app_started = false;
18+
// Tracks the physical finger, independent of the app lifecycle: a finger on the screen halts the
19+
// timeout even across focus pause/resume, and liftoff restarts it only if nothing else pauses it.
20+
bool s_touch_held = false;
1821

1922
#ifndef CONFIG_NO_WATCH_TIMEOUT
2023
static const int WATCHFACE_TIMEOUT_MS = 30000;
@@ -33,7 +36,7 @@ static void prv_start_timer(bool create) {
3336
s_timer = new_timer_create();
3437
}
3538

36-
if (s_timer != TIMER_INVALID_ID && !s_app_paused && s_app_started) {
39+
if (s_timer != TIMER_INVALID_ID && !s_app_paused && !s_touch_held && s_app_started) {
3740
bool success = new_timer_start(s_timer, WATCHFACE_TIMEOUT_MS, prv_timeout_expired,
3841
NULL, 0 /* flags */);
3942
PBL_ASSERTN(success);
@@ -77,3 +80,17 @@ void app_idle_timeout_refresh(void) {
7780
prv_start_timer(false /* do not create a timer */);
7881
#endif
7982
}
83+
84+
void app_idle_timeout_touch_down(void) {
85+
s_touch_held = true;
86+
if (s_timer != TIMER_INVALID_ID) {
87+
new_timer_stop(s_timer);
88+
}
89+
}
90+
91+
void app_idle_timeout_touch_up(void) {
92+
s_touch_held = false;
93+
#ifndef CONFIG_NO_WATCH_TIMEOUT
94+
prv_start_timer(false /* do not create a timer */);
95+
#endif
96+
}

src/fw/shell/normal/app_idle_timeout.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,11 @@ void app_idle_timeout_resume(void);
2121
//! is safe to call even if the idle timeout wasn't running previously.
2222
void app_idle_timeout_refresh(void);
2323

24+
//! Halt the idle timeout while a finger is on the touchscreen. Unlike pause/resume, this state
25+
//! composes with the focus-driven pause. Safe to call even if the idle timeout isn't running.
26+
void app_idle_timeout_touch_down(void);
27+
28+
//! Restart the idle timeout when the finger lifts off. Safe to call even if the idle timeout
29+
//! isn't running or no touch-down was observed.
30+
void app_idle_timeout_touch_up(void);
31+

src/fw/shell/prf/stubs.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ void watchface_handle_button_event(PebbleEvent *e) {
5252
void app_idle_timeout_refresh(void) {
5353
}
5454

55+
void app_idle_timeout_touch_down(void) {
56+
}
57+
58+
void app_idle_timeout_touch_up(void) {
59+
}
60+
5561
PebblePhoneCaller* phone_call_util_create_caller(const char *number, const char *name) {
5662
return NULL;
5763
}

src/fw/shell/sdk/stubs.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ void app_idle_timeout_refresh(void) {
2121
void app_idle_timeout_stop(void) {
2222
}
2323

24+
void app_idle_timeout_touch_down(void) {
25+
}
26+
27+
void app_idle_timeout_touch_up(void) {
28+
}
29+
2430
void watchface_start_low_power(bool enable) {
2531
}
2632

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/* SPDX-FileCopyrightText: 2026 Core Devices LLC */
2+
/* SPDX-License-Identifier: Apache-2.0 */
3+
4+
#include "clar.h"
5+
6+
#include "shell/normal/app_idle_timeout.h"
7+
8+
#include "kernel/event_loop.h"
9+
#include "shell/normal/watchface.h"
10+
#include "shell/shell.h"
11+
12+
// Stubs
13+
/////////////////////////////////////////////////////////////////////////
14+
#include "stubs_logging.h"
15+
#include "stubs_passert.h"
16+
17+
#include "fake_new_timer.h"
18+
19+
static int s_watchface_launch_count;
20+
21+
void launcher_task_add_callback(CallbackEventCallback callback, void *data) {
22+
callback(data);
23+
}
24+
25+
const CompositorTransition *shell_get_watchface_compositor_animation(bool watchface_is_destination) {
26+
return NULL;
27+
}
28+
29+
void watchface_launch_default(const CompositorTransition *animation) {
30+
s_watchface_launch_count++;
31+
}
32+
33+
// Module state under test
34+
/////////////////////////////////////////////////////////////////////////
35+
extern TimerID s_timer;
36+
extern bool s_app_paused;
37+
extern bool s_touch_held;
38+
39+
static bool prv_is_scheduled(void) {
40+
return stub_new_timer_is_scheduled(s_timer);
41+
}
42+
43+
// Tests
44+
/////////////////////////////////////////////////////////////////////////
45+
46+
void test_app_idle_timeout__initialize(void) {
47+
s_watchface_launch_count = 0;
48+
app_idle_timeout_stop();
49+
s_app_paused = false;
50+
s_touch_held = false;
51+
}
52+
53+
void test_app_idle_timeout__cleanup(void) {
54+
app_idle_timeout_stop();
55+
stub_new_timer_cleanup();
56+
}
57+
58+
void test_app_idle_timeout__start_schedules(void) {
59+
app_idle_timeout_start();
60+
cl_assert(prv_is_scheduled());
61+
}
62+
63+
void test_app_idle_timeout__touch_hold_halts_until_liftoff(void) {
64+
app_idle_timeout_start();
65+
66+
app_idle_timeout_touch_down();
67+
cl_assert(!prv_is_scheduled());
68+
69+
app_idle_timeout_touch_up();
70+
cl_assert(prv_is_scheduled());
71+
}
72+
73+
void test_app_idle_timeout__refresh_during_hold_stays_halted(void) {
74+
app_idle_timeout_start();
75+
app_idle_timeout_touch_down();
76+
77+
app_idle_timeout_refresh();
78+
cl_assert(!prv_is_scheduled());
79+
80+
app_idle_timeout_touch_up();
81+
cl_assert(prv_is_scheduled());
82+
}
83+
84+
void test_app_idle_timeout__hold_composes_with_focus_pause(void) {
85+
app_idle_timeout_start();
86+
87+
// Focus lost mid-hold: liftoff must not restart the timer while paused.
88+
app_idle_timeout_touch_down();
89+
app_idle_timeout_pause();
90+
app_idle_timeout_touch_up();
91+
cl_assert(!prv_is_scheduled());
92+
app_idle_timeout_resume();
93+
cl_assert(prv_is_scheduled());
94+
95+
// Focus regained mid-hold: resume must not restart the timer while held.
96+
app_idle_timeout_pause();
97+
app_idle_timeout_touch_down();
98+
app_idle_timeout_resume();
99+
cl_assert(!prv_is_scheduled());
100+
app_idle_timeout_touch_up();
101+
cl_assert(prv_is_scheduled());
102+
}
103+
104+
void test_app_idle_timeout__liftoff_without_touchdown_is_harmless(void) {
105+
app_idle_timeout_start();
106+
app_idle_timeout_touch_up();
107+
cl_assert(prv_is_scheduled());
108+
}
109+
110+
void test_app_idle_timeout__start_during_hold_waits_for_liftoff(void) {
111+
app_idle_timeout_touch_down();
112+
app_idle_timeout_start();
113+
cl_assert(!prv_is_scheduled());
114+
115+
app_idle_timeout_touch_up();
116+
cl_assert(prv_is_scheduled());
117+
}
118+
119+
void test_app_idle_timeout__expiry_launches_watchface(void) {
120+
app_idle_timeout_start();
121+
cl_assert(stub_new_timer_fire(s_timer));
122+
cl_assert_equal_i(s_watchface_launch_count, 1);
123+
}

tests/fw/shell/normal/wscript_build

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ clar(ctx,
44
sources_ant_glob = " src/fw/shell/normal/system_app_state_machine.c",
55
test_sources_ant_glob = "test_normal_system_app_state_machine.c")
66

7+
clar(ctx,
8+
sources_ant_glob = " src/fw/shell/normal/app_idle_timeout.c",
9+
test_sources_ant_glob = "test_app_idle_timeout.c")
10+
711
clar(ctx,
812
sources_ant_glob = " src/fw/shell/normal/battery_ui_fsm.c"
913
" src/fw/services/battery/voltage/battery_curve.c",

0 commit comments

Comments
 (0)