/* * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "app.h" #include "applib/app_heap_analytics.h" #include "applib/graphics/graphics_private.h" #include "applib/ui/app_window_stack.h" #include "applib/ui/window_stack.h" #include "applib/ui/window_private.h" #include "mcu/fpu.h" #include "process_management/app_manager.h" #include "process_state/app_state/app_state.h" #include "syscall/syscall.h" #include "system/logging.h" #include "system/profiler.h" static void prv_render_app(void) { WindowStack *stack = app_state_get_window_stack(); GContext *ctx = app_state_get_graphics_context(); if (!window_stack_is_animating(stack)) { SYS_PROFILER_NODE_START(render_app); window_render(app_window_stack_get_top_window(), ctx); SYS_PROFILER_NODE_STOP(render_app); } else { // TODO: PBL-17645 render container layer instead of the two windows WindowTransitioningContext *transition_context = &stack->transition_context; if (transition_context->implementation->render) { transition_context->implementation->render(transition_context, ctx); } } *app_state_get_framebuffer_render_pending() = true; PebbleEvent event = { .type = PEBBLE_RENDER_READY_EVENT, }; sys_send_pebble_event_to_kernel(&event); } static bool prv_window_is_render_scheduled(Window *window) { return window && window->is_render_scheduled; } static bool prv_app_is_render_scheduled() { Window *top_window = app_window_stack_get_top_window(); if (prv_window_is_render_scheduled(top_window)) { return true; } WindowStack *stack = app_state_get_window_stack(); if (!window_stack_is_animating(stack)) { return false; } WindowTransitioningContext *transition_ctx = &stack->transition_context; return prv_window_is_render_scheduled(transition_ctx->window_from) || prv_window_is_render_scheduled(transition_ctx->window_to); } void app_request_render(void) { Window *window = app_window_stack_get_top_window(); if (window) { window_schedule_render(window); } } //! Tasks that have to be done in between each event. static NOINLINE void event_loop_upkeep(void) { // Check to see if the most recent event caused us to pop our final window. If that's the case, we need to // kill ourselves. if (app_window_stack_count() == 0) { PBL_LOG(LOG_LEVEL_DEBUG, "No more windows, killing current app"); PebbleEvent event = { .type = PEBBLE_PROCESS_KILL_EVENT, .kill = { .gracefully = true, .task=PebbleTask_App } }; sys_send_pebble_event_to_kernel(&event); return; } // Check to see if handling the previous event requires us to rerender ourselves. if (prv_app_is_render_scheduled() && !*app_state_get_framebuffer_render_pending()) { prv_render_app(); } } static void prv_app_will_focus_handler(PebbleEvent *e, void *context) { Window *window = app_window_stack_get_top_window(); if (e->app_focus.in_focus) { if (window) { // Do not call 'appear' handler on window displacing modal window window_set_on_screen(window, true, false); window_render(window, app_state_get_graphics_context()); } click_manager_reset(app_state_get_click_manager()); } else if (window) { // Do not call 'disappear' handler on window displaced by modal window window_set_on_screen(window, false, false); } } static void prv_app_button_down_handler(PebbleEvent *e, void *context) { WindowStack *app_window_stack = app_state_get_window_stack(); if (window_stack_is_animating(app_window_stack)) { return; } sys_analytics_inc(ANALYTICS_APP_METRIC_BUTTONS_PRESSED_COUNT, AnalyticsClient_App); if (e->button.button_id == BUTTON_ID_BACK && !app_window_stack_get_top_window()->overrides_back_button) { // a transition of NULL means we will use the stored pop transition for this stack item window_stack_pop_with_transition(app_window_stack, NULL /* transition */); return; } click_recognizer_handle_button_down( &app_state_get_click_manager()->recognizers[e->button.button_id]); } static void prv_app_button_up_handler(PebbleEvent *e, void *context) { if (window_stack_is_animating(app_state_get_window_stack())) { return; } click_recognizer_handle_button_up( &app_state_get_click_manager()->recognizers[e->button.button_id]); } // this handler is called via the legacy2_status_bar_change_event and // will update the status bar once a minute for non-fullscreen legacy2 apps static void prv_legacy2_status_bar_handler(PebbleEvent *e, void *context) { Window *window = app_window_stack_get_top_window(); // only force render if we're not fullscreen if (!window->is_fullscreen) { // a little logic to only force update when the minute changes ApplibInternalEventsInfo *events_info = app_state_get_applib_internal_events_info(); struct tm currtime; sys_localtime_r(&e->clock_tick.tick_time, &currtime); const int minute_of_day = (currtime.tm_hour * 60) + currtime.tm_min; if (events_info->minute_of_last_legacy2_statusbar_change != minute_of_day) { events_info->minute_of_last_legacy2_statusbar_change = minute_of_day; window_schedule_render(window); } } } static void prv_legacy2_status_bar_timer_subscribe(void) { // we only need this tick event if we are a legacy2 app if (process_manager_compiled_with_legacy2_sdk()) { ApplibInternalEventsInfo *events_info = app_state_get_applib_internal_events_info(); // Initialize the state for the status bar handler. events_info->minute_of_last_legacy2_statusbar_change = -1; events_info->legacy2_status_bar_change_event = (EventServiceInfo) { .type = PEBBLE_TICK_EVENT, .handler = prv_legacy2_status_bar_handler, }; event_service_client_subscribe( &events_info->legacy2_status_bar_change_event); } // NOTE: We could be super fancy and register and unregister when the fullscreen // status changes, but it's probably not worth it as we'll be waking up once a // minute anyway to update the face itself and it will happen as part of the same interval } static void prv_legacy2_status_bar_timer_unsubscribe(void) { // we should only unsubscribe if we subscribed in the first place if (process_manager_compiled_with_legacy2_sdk()) { ApplibInternalEventsInfo *events_info = app_state_get_applib_internal_events_info(); event_service_client_unsubscribe( &events_info->legacy2_status_bar_change_event); } } static void prv_app_callback_handler(PebbleEvent *e) { e->callback.callback(e->callback.data); } static NOINLINE void prv_handle_deinit_event(void) { ApplibInternalEventsInfo *events_info = app_state_get_applib_internal_events_info(); event_service_client_unsubscribe(&events_info->will_focus_event); event_service_client_unsubscribe(&events_info->button_down_event); event_service_client_unsubscribe(&events_info->button_up_event); prv_legacy2_status_bar_timer_unsubscribe(); // a no-op on sdk3+ applications WindowStack *app_window_stack = app_state_get_window_stack(); window_stack_lock_push(app_window_stack); window_stack_pop_all(app_window_stack, false); window_stack_unlock_push(app_window_stack); } // Get the app_id for the current app or worker // @return INSTALL_ID_INVALID if unsuccessful AppInstallId app_get_app_id(void) { // Only support from app or workers PebbleTask task = pebble_task_get_current(); if ((task != PebbleTask_App) && (task != PebbleTask_Worker)) { APP_LOG(APP_LOG_LEVEL_ERROR, "Only supported from app or worker tasks"); return INSTALL_ID_INVALID; } // Get the app id if (task == PebbleTask_App) { return sys_app_manager_get_current_app_id(); } else { return sys_worker_manager_get_current_worker_id(); } } void app_event_loop_common(void) { // Register our event handlers before we do anything else. Registering for an event requires // an event being sent to the kernel and therefore should be done before any other events are // generated by us to ensure we don't miss out on anything. ApplibInternalEventsInfo *events_info = app_state_get_applib_internal_events_info(); events_info->will_focus_event = (EventServiceInfo) { .type = PEBBLE_APP_WILL_CHANGE_FOCUS_EVENT, .handler = prv_app_will_focus_handler, }; events_info->button_down_event = (EventServiceInfo) { .type = PEBBLE_BUTTON_DOWN_EVENT, .handler = prv_app_button_down_handler, }; events_info->button_up_event = (EventServiceInfo) { .type = PEBBLE_BUTTON_UP_EVENT, .handler = prv_app_button_up_handler, }; event_service_client_subscribe(&events_info->will_focus_event); event_service_client_subscribe(&events_info->button_down_event); event_service_client_subscribe(&events_info->button_up_event); prv_legacy2_status_bar_timer_subscribe(); // a no-op on sdk3+ applications event_loop_upkeep(); // Event loop: while (1) { PebbleEvent event; sys_get_pebble_event(&event); if (event.type == PEBBLE_PROCESS_DEINIT_EVENT) { prv_handle_deinit_event(); // We're done here. Return the app's main function. event_cleanup(&event); return; } else if (event.type == PEBBLE_CALLBACK_EVENT) { prv_app_callback_handler(&event); } else if (event.type == PEBBLE_RENDER_REQUEST_EVENT) { app_request_render(); } else if (event.type == PEBBLE_RENDER_FINISHED_EVENT) { *app_state_get_framebuffer_render_pending() = false; } else { event_service_client_handle_event(&event); } mcu_fpu_cleanup(); event_cleanup(&event); event_loop_upkeep(); } } void app_event_loop(void) { app_event_loop_common(); app_heap_analytics_log_stats_to_app_heartbeat(false /* is_rocky_app */); }