Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson
2024-12-12 16:43:03 -08:00
committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
/*
* 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_idle_timeout.h"
#include "kernel/event_loop.h"
#include "os/tick.h"
#include "services/common/new_timer/new_timer.h"
#include "shell/normal/watchface.h"
#include "shell/shell.h"
#include "system/logging.h"
#include "system/passert.h"
static const int WATCHFACE_TIMEOUT_MS = 30000;
TimerID s_timer;
bool s_app_paused = false;
bool s_app_started = false;
#ifndef NO_WATCH_TIMEOUT
static void prv_kernel_callback_watchface_launch(void* data) {
watchface_launch_default(shell_get_watchface_compositor_animation(true /* watchface_is_dest */));
}
static void prv_timeout_expired(void *cb_data) {
PBL_LOG(LOG_LEVEL_DEBUG, "App idle timeout hit! launching watchface");
launcher_task_add_callback(prv_kernel_callback_watchface_launch, NULL);
}
static void prv_start_timer(bool create) {
if (create) {
s_timer = new_timer_create();
}
if (s_timer != TIMER_INVALID_ID && !s_app_paused && s_app_started) {
bool success = new_timer_start(s_timer, WATCHFACE_TIMEOUT_MS, prv_timeout_expired,
NULL, 0 /* flags */);
PBL_ASSERTN(success);
}
}
#endif
void app_idle_timeout_start(void) {
PBL_ASSERTN(s_timer == TIMER_INVALID_ID);
s_app_started = true;
#ifndef NO_WATCH_TIMEOUT
prv_start_timer(true /* create a timer */);
#endif
}
void app_idle_timeout_stop(void) {
if (s_timer != TIMER_INVALID_ID) {
new_timer_delete(s_timer);
s_timer = TIMER_INVALID_ID;
s_app_started = false;
}
}
void app_idle_timeout_pause(void) {
if (s_timer != TIMER_INVALID_ID) {
new_timer_stop(s_timer);
}
s_app_paused = true;
}
void app_idle_timeout_resume(void) {
s_app_paused = false;
#ifndef NO_WATCH_TIMEOUT
prv_start_timer(false /* do not create a timer */);
#endif
}
void app_idle_timeout_refresh(void) {
#ifndef NO_WATCH_TIMEOUT
prv_start_timer(false /* do not create a timer */);
#endif
}

View File

@@ -0,0 +1,36 @@
/*
* 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.
*/
#pragma once
//! Start using the idle timeout for the current app.
void app_idle_timeout_start(void);
//! Stop using the idle timeout for the current app. This is safe to call even if the idle timeout wasn't running.
void app_idle_timeout_stop(void);
//! Pause the idle timeout for the current app. This is safe to call even if the idle timeout wasn't running
//! previously.
void app_idle_timeout_pause(void);
//! Resume the idle timeout for the current app. This is safe to call even if the idle timeout wasn't running
//! previously.
void app_idle_timeout_resume(void);
//! Reset the timeout. Call this whenever there is activity that should prevent the idle timeout from firing. This
//! is safe to call even if the idle timeout wasn't running previously.
void app_idle_timeout_refresh(void);

View File

@@ -0,0 +1,188 @@
/*
* 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 "battery_ui.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "applib/ui/dialogs/dialog_private.h"
#include "applib/ui/dialogs/simple_dialog.h"
#include "applib/ui/window_stack.h"
#include "applib/ui/ui.h"
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "kernel/ui/kernel_ui.h"
#include "kernel/ui/modals/modal_manager.h"
#include "process_management/app_manager.h"
#include "resource/resource_ids.auto.h"
#include "services/common/battery/battery_curve.h"
#include "services/common/clock.h"
#include "services/common/i18n/i18n.h"
#include "services/common/light.h"
#include "system/logging.h"
#include "util/time/time.h"
typedef void (*DialogUpdateFn)(Dialog *, void *);
static Dialog *s_dialog = NULL;
typedef struct {
uint32_t percent;
GColor background_color;
ResourceId warning_icon;
} BatteryWarningDisplayData;
// UI Callbacks
///////////////////////
static const GColor s_warning_color[] = {
{ .argb = GColorLightGrayARGB8 },
{ .argb = GColorRedARGB8 },
};
static const ResourceId s_warning_icon[] = {
RESOURCE_ID_BATTERY_ICON_LOW_LARGE,
RESOURCE_ID_BATTERY_ICON_VERY_LOW_LARGE
};
static void prv_update_ui_fully_charged(Dialog *dialog, void *ignored) {
dialog_set_text(dialog, i18n_get("Fully Charged", dialog));
dialog_set_background_color(dialog, GColorKellyGreen);
dialog_set_icon(dialog, RESOURCE_ID_BATTERY_ICON_FULL_LARGE);
}
static void prv_update_ui_charging(Dialog *dialog, void *ignored) {
dialog_set_text(dialog, i18n_get("Charging", dialog));
dialog_set_background_color(dialog, GColorLightGray);
dialog_set_icon(dialog, RESOURCE_ID_BATTERY_ICON_CHARGING_LARGE);
}
static void prv_update_ui_warning(Dialog *dialog, void *context) {
const BatteryWarningDisplayData *data = context;
const uint32_t percent = data->percent;
dialog_set_background_color(dialog, data->background_color);
const size_t warning_length = 64;
char buffer[warning_length];
const uint32_t battery_hours_left = battery_curve_get_hours_remaining(percent);
const char *message = clock_get_relative_daypart_string(rtc_get_time(), battery_hours_left);
if (message) {
snprintf(buffer, warning_length, i18n_get("Powered 'til %s", dialog),
i18n_get(message, dialog));
dialog_set_text(dialog, buffer);
}
dialog_set_icon(dialog, data->warning_icon);
}
static void prv_dialog_on_unload(void *context) {
Dialog *dialog = context;
i18n_free_all(dialog);
if (dialog == s_dialog) {
s_dialog = NULL;
}
}
static void prv_display_modal(WindowStack *stack, DialogUpdateFn update_fn, void *data) {
if (s_dialog) {
update_fn(s_dialog, data);
return;
}
SimpleDialog *new_simple_dialog = simple_dialog_create(
WINDOW_NAME("Battery Status"));
Dialog *new_dialog = simple_dialog_get_dialog(new_simple_dialog);
dialog_set_callbacks(new_dialog, &(DialogCallbacks) {
.unload = prv_dialog_on_unload,
}, NULL);
update_fn(new_dialog, data);
Dialog *old_dialog = s_dialog;
s_dialog = new_dialog;
simple_dialog_push(new_simple_dialog, stack);
#if PBL_ROUND
// For circular display, to fit some battery_ui messages requires 3 lines
// Simple dialog only allows up to 2 lines, so adjust here
// This has to occur after the dialog push has been called
TextLayer *text_layer = &new_simple_dialog->dialog.text_layer;
GContext *ctx = graphics_context_get_current_context();
const int font_height = fonts_get_font_height(text_layer->font);
const int text_cap_height = fonts_get_font_cap_offset(text_layer->font);
const int max_text_height = 2 * font_height + text_cap_height;
const int32_t text_height = text_layer_get_content_size(ctx, text_layer).h;
if (text_height > max_text_height) {
// Values used below were to improve visual aesthetics and were reviewed by design
const int num_lines = 3;
const int line_spacing_delta = -4;
const int text_shift_y = -2;
const int text_box_height = (font_height + text_cap_height) * num_lines +
line_spacing_delta * (num_lines - 1);
const int text_flow_inset = 6; // Modify to allow longer central lines
text_layer_enable_screen_text_flow_and_paging(text_layer, text_flow_inset);
text_layer_set_size(text_layer, GSize(DISP_COLS, text_box_height));
text_layer->layer.frame.origin.y += text_shift_y;
text_layer_set_line_spacing_delta(text_layer, line_spacing_delta);
}
#endif
if (old_dialog) {
dialog_pop(old_dialog);
}
}
// Public API
////////////////////
void battery_ui_display_plugged(void) {
// If we're plugged in for charging, we want to alert the user of this,
// but we don't want to overlay ourselves over anything they may have
// on the screen at the moment.
WindowStack *stack = modal_manager_get_window_stack(ModalPriorityGeneric);
prv_display_modal(stack, prv_update_ui_charging, NULL);
}
void battery_ui_display_fully_charged(void) {
// If we're plugged in (charged), we want to alert the user of this,
// but we don't want to overlay ourselves over anything they may have
// on the screen at the moment.
WindowStack *stack = modal_manager_get_window_stack(ModalPriorityGeneric);
prv_display_modal(stack, prv_update_ui_fully_charged, NULL);
}
void battery_ui_display_warning(uint32_t percent, BatteryUIWarningLevel warning_level) {
// If we're not plugged in, that means we hit a critical power notification,
// so we want to alert the user, subverting any non-critical windows they
// have on the screen.
WindowStack *stack = modal_manager_get_window_stack(ModalPriorityAlert);
BatteryWarningDisplayData display_data = {
.percent = percent,
.background_color = s_warning_color[warning_level],
.warning_icon = s_warning_icon[warning_level],
};
prv_display_modal(stack, prv_update_ui_warning, &display_data);
}
void battery_ui_dismiss_modal(void) {
if (s_dialog) {
dialog_pop(s_dialog);
s_dialog = NULL;
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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.
*/
#pragma once
#include "applib/graphics/gtypes.h"
#include "services/common/battery/battery_monitor.h"
typedef enum BatteryUIWarningLevel {
BatteryUIWarningLevel_None = -1,
BatteryUIWarningLevel_Low,
BatteryUIWarningLevel_VeryLow
} BatteryUIWarningLevel;
//! Process the incoming battery state change notification
void battery_ui_handle_state_change_event(PreciseBatteryChargeState new_state);
//! Handle shutting down the watch.
//!
//! If the watch is plugged in at the time, a "shut down while charging" UI is
//! displayed to give the user feedback on the charge state. Standby will be
//! entered once the watch is unplugged.
void battery_ui_handle_shut_down(void);
//! Show the 'battery charging' modal dialog
void battery_ui_display_plugged(void);
//! Show the 'battery charged' modal dialog
void battery_ui_display_fully_charged(void);
//! Show the 'battery critical' modal dialog
void battery_ui_display_warning(uint32_t percent, BatteryUIWarningLevel warning_level);
//! Dismiss the battery UI modal window.
void battery_ui_dismiss_modal(void);

View File

@@ -0,0 +1,292 @@
/*
* 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 "battery_ui.h"
#include <stdint.h>
#include "applib/ui/vibes.h"
#include "apps/system_app_ids.h"
#include "apps/system_apps/battery_critical_app.h"
#include "kernel/low_power.h"
#include "kernel/ui/modals/modal_manager.h"
#include "kernel/util/standby.h"
#include "process_management/app_manager.h"
#include "services/common/battery/battery_curve.h"
#include "services/common/status_led.h"
#include "services/common/vibe_pattern.h"
#include "services/normal/notifications/do_not_disturb.h"
#include "services/normal/vibes/vibe_intensity.h"
#include "shell/normal/watchface.h"
#include "util/ratio.h"
#include "util/size.h"
// The Battery UI state machine keeps track of when to notify the user of a
// change in battery charge state, and when to automatically dismiss the status
// modal window.
#define MAX_TRANSITIONS 6
typedef void (*EntryFunc)(void *);
typedef void (*ExitFunc)(void);
typedef enum BatteryUIStateID {
BatteryInvalid,
BatteryGood,
BatteryWarning,
BatteryLowPower,
BatteryCritical,
BatteryCharging,
BatteryFullyCharged, // plugged but not charging (aka 100%)
BatteryShutdownCharging
} BatteryUIStateID;
typedef struct BatteryUIState {
EntryFunc enter;
ExitFunc exit;
BatteryUIStateID next_state[MAX_TRANSITIONS];
} BatteryUIState;
static void prv_display_warning(void *data);
static void prv_dismiss_warning(void);
static void prv_enter_low_power(void *ignored);
static void prv_exit_low_power(void);
static void prv_enter_critical(void *ignored);
static void prv_exit_critical(void);
static void prv_display_plugged(void *data);
static void prv_dismiss_plugged(void);
static void prv_display_fully_charged(void *data);
static void prv_dismiss_fully_charged(void);
// TODO PBL-39883: Replace w/ QUIRK_RESET_ON_SHUTDOWN_WHILE_CHARGING once arbitrary prefixes land
#if PLATFORM_TINTIN || PLATFORM_SILK
static void prv_shutdown(void *ignored);
#else
static void prv_enter_shutdown_charging(void *ignored);
#endif
static const BatteryUIState ui_states[] = {
[BatteryGood] = { .next_state = {
BatteryWarning, BatteryLowPower, BatteryCritical, BatteryCharging, BatteryFullyCharged
}},
[BatteryWarning] = { .enter = prv_display_warning, .exit = prv_dismiss_warning, .next_state = {
BatteryGood, BatteryWarning, BatteryLowPower, BatteryCharging
}},
[BatteryLowPower] = { .enter = prv_enter_low_power, .exit = prv_exit_low_power, .next_state = {
BatteryWarning, BatteryCritical, BatteryCharging
}},
[BatteryCritical] = { .enter = prv_enter_critical, .exit = prv_exit_critical, .next_state = {
BatteryLowPower, BatteryCharging
}},
[BatteryCharging] = { .enter = prv_display_plugged, .exit = prv_dismiss_plugged, .next_state = {
BatteryGood, BatteryWarning, BatteryLowPower,
BatteryCritical, BatteryFullyCharged, BatteryShutdownCharging
}},
[BatteryFullyCharged] = { .enter = prv_display_fully_charged, .exit = prv_dismiss_fully_charged,
.next_state = {
BatteryGood, BatteryWarning, BatteryLowPower, BatteryCritical, BatteryShutdownCharging
}},
// TODO PBL-39883: Replace w/ QUIRK_RESET_ON_SHUTDOWN_WHILE_CHARGING once arbitrary prefixes land
#if PLATFORM_TINTIN || PLATFORM_SILK
[BatteryShutdownCharging] = { .enter = prv_shutdown }
#else
[BatteryShutdownCharging] = { .enter = prv_enter_shutdown_charging }
#endif
};
static BatteryUIStateID s_state = BatteryGood;
static BatteryUIWarningLevel s_warning_points_index = -1;
#if PLATFORM_SPALDING
/* first warning for S4 is at 12 hours remaining, second at 6 hours remaining */
static const uint8_t s_warning_points[] = { 12, 6 };
#else
/* first warning is at 18 hours remaining, second at 12 hours remaining */
static const uint8_t s_warning_points[] = { 18, 12 };
#endif
// State functions
static void prv_display_warning(void *data) {
const uint8_t percent = ratio32_to_percent(((PreciseBatteryChargeState *)data)->charge_percent);
bool new_warning = false;
const BatteryUIWarningLevel num_points = ARRAY_LENGTH(s_warning_points) - 1;
while (s_warning_points_index < num_points && (percent <=
battery_curve_get_percent_remaining(s_warning_points[s_warning_points_index + 1]))) {
s_warning_points_index++;
new_warning = true;
}
if (new_warning) {
if (!do_not_disturb_is_active()) {
vibes_short_pulse();
}
battery_ui_display_warning(percent, s_warning_points_index);
}
}
static void prv_dismiss_warning(void) {
battery_ui_dismiss_modal();
s_warning_points_index = -1;
}
static void prv_enter_low_power(void *ignored) {
#ifndef RECOVERY_FW
watchface_start_low_power();
modal_manager_pop_all_below_priority(ModalPriorityAlarm);
modal_manager_set_min_priority(ModalPriorityAlarm);
// Override the vibe intensity to Medium in low-power mode
vibes_set_default_vibe_strength(get_strength_for_intensity(VibeIntensityMedium));
#else
app_manager_launch_new_app(&(AppLaunchConfig) {
.md = prf_low_power_app_get_info(),
});
#endif
}
static void prv_exit_low_power(void) {
#ifndef RECOVERY_FW
modal_manager_set_min_priority(ModalPriorityMin);
watchface_launch_default(NULL);
vibe_intensity_set(vibe_intensity_get());
#else
app_manager_close_current_app(true);
#endif
}
static void prv_enter_critical(void *ignored) {
if (!do_not_disturb_is_active()) {
vibes_short_pulse();
}
// in case there is a warning on screen
modal_manager_pop_all();
modal_manager_set_min_priority(ModalPriorityMax);
app_manager_put_launch_app_event(&(AppLaunchEventConfig) {
.id = APP_ID_BATTERY_CRITICAL,
});
}
static void prv_exit_critical(void) {
app_manager_close_current_app(true);
modal_manager_set_min_priority(ModalPriorityMin);
}
static void prv_display_plugged(void *data) {
if (!do_not_disturb_is_active()) {
vibes_short_pulse();
}
battery_ui_display_plugged();
status_led_set(StatusLedState_Charging);
}
static void prv_dismiss_plugged(void) {
battery_ui_dismiss_modal();
status_led_set(StatusLedState_Off);
}
static void prv_display_fully_charged(void *data) {
battery_ui_display_fully_charged();
status_led_set(StatusLedState_FullyCharged);
}
static void prv_dismiss_fully_charged(void) {
battery_ui_dismiss_modal();
status_led_set(StatusLedState_Off);
}
// TODO PBL-39883: Replace w/ QUIRK_RESET_ON_SHUTDOWN_WHILE_CHARGING once arbitrary prefixes land
#if PLATFORM_TINTIN || PLATFORM_SILK
static void prv_shutdown(void *ignored) {
battery_ui_handle_shut_down();
}
#else
static void prv_enter_shutdown_charging(void *ignored) {
app_manager_put_launch_app_event(&(AppLaunchEventConfig) {
.id = APP_ID_SHUTDOWN_CHARGING,
});
}
#endif
// Internals
static void prv_transition(BatteryUIStateID next_state, void *data) {
if (s_state != next_state) {
// All self-transitions are internal.
// A state's entry function is a its only valid action.
// The exit function is only called on actual state changes.
if (ui_states[s_state].exit) {
ui_states[s_state].exit();
}
s_state = next_state;
}
if (ui_states[s_state].enter) {
ui_states[s_state].enter(data);
}
}
static bool prv_is_valid_transition(BatteryUIStateID next_state) {
const uint8_t count = ARRAY_LENGTH(ui_states[s_state].next_state);
for (int i = 0; i < count; i++) {
if (ui_states[s_state].next_state[i] == next_state) {
return true;
}
}
return false;
}
static BatteryUIStateID prv_get_state(PreciseBatteryChargeState *state) {
// TODO: Refactor?
if (state->is_plugged) {
// Don't use the PreciseBatteryChargeState definition of is_charging, as it maps to the
// result of @see battery_charge_controller_thinks_we_are_charging instead of the actual
// user-facing definition of charging.
const uint32_t is_charging = battery_get_charge_state().is_charging;
return is_charging ? BatteryCharging : BatteryFullyCharged;
} else if (battery_monitor_critical_lockout()) {
return BatteryCritical;
} else if (low_power_is_active()) {
return BatteryLowPower;
} else if (ratio32_to_percent(state->charge_percent) <=
battery_curve_get_percent_remaining(s_warning_points[0])) {
return BatteryWarning;
} else {
return BatteryGood;
}
}
void battery_ui_handle_state_change_event(PreciseBatteryChargeState charge_state) {
BatteryUIStateID next_state = prv_get_state(&charge_state);
if (prv_is_valid_transition(next_state)) {
prv_transition(next_state, &charge_state);
}
}
void battery_ui_handle_shut_down(void) {
if (s_state != BatteryCharging) {
enter_standby(RebootReasonCode_ShutdownMenuItem);
} else {
prv_transition(BatteryShutdownCharging, NULL);
}
}
void battery_ui_reset_fsm_for_tests(void) {
s_state = BatteryGood;
s_warning_points_index = -1;
}

View File

@@ -0,0 +1,127 @@
/*
* 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.
*/
#if PLATFORM_SPALDING
#include "display_calibration_prompt.h"
#include "applib/ui/dialogs/confirmation_dialog.h"
#include "apps/system_apps/settings/settings_display_calibration.h"
#include "kernel/event_loop.h"
#include "kernel/ui/modals/modal_manager.h"
#include "mfg/mfg_info.h"
#include "mfg/mfg_serials.h"
#include "resource/resource_ids.auto.h"
#include "services/common/i18n/i18n.h"
#include "services/common/new_timer/new_timer.h"
#include "shell/prefs.h"
#include "util/size.h"
// The calibration screen will be changing the screen offsets, so it's best that it remains on top
// of most other modals (generic, alerts, etc) to prevent confusion about the screen's alignment.
static const ModalPriority MODAL_PRIORITY = ModalPriorityCritical;
static void prv_calibrate_confirm_pop(ClickRecognizerRef recognizer, void *context) {
i18n_free_all(context);
confirmation_dialog_pop((ConfirmationDialog *)context);
}
static void prv_calibrate_confirm_cb(ClickRecognizerRef recognizer, void *context) {
settings_display_calibration_push(modal_manager_get_window_stack(MODAL_PRIORITY));
prv_calibrate_confirm_pop(recognizer, context);
}
static void prv_calibrate_click_config(void *context) {
window_single_click_subscribe(BUTTON_ID_UP, prv_calibrate_confirm_cb);
window_single_click_subscribe(BUTTON_ID_DOWN, prv_calibrate_confirm_pop);
window_single_click_subscribe(BUTTON_ID_BACK, prv_calibrate_confirm_pop);
}
static TimerID s_timer = TIMER_INVALID_ID;
static void prv_push_calibration_dialog(void *data) {
shell_prefs_set_should_prompt_display_calibration(false);
ConfirmationDialog *confirmation_dialog = confirmation_dialog_create("Calibrate Prompt");
Dialog *dialog = confirmation_dialog_get_dialog(confirmation_dialog);
dialog_set_text(dialog, i18n_get("Your screen may need calibration. Calibrate it now?",
confirmation_dialog));
dialog_set_background_color(dialog, GColorMediumAquamarine);
dialog_set_icon(dialog, RESOURCE_ID_GENERIC_PIN_TINY);
confirmation_dialog_set_click_config_provider(confirmation_dialog,
prv_calibrate_click_config);
confirmation_dialog_push(confirmation_dialog,
modal_manager_get_window_stack(MODAL_PRIORITY));
}
static bool prv_display_has_user_offset(void) {
GPoint display_offset = shell_prefs_get_display_offset();
GPoint mfg_display_offset = mfg_info_get_disp_offsets();
return (!gpoint_equal(&display_offset, &mfg_display_offset));
}
static void prv_timer_callback(void *data) {
new_timer_delete(s_timer);
s_timer = TIMER_INVALID_ID;
// last check: make sure we need to display the prompt in case something changed in the
// time that the timer was waiting.
if (!shell_prefs_should_prompt_display_calibration()) {
return;
}
launcher_task_add_callback(prv_push_calibration_dialog, NULL);
}
T_STATIC bool prv_is_known_misaligned_serial_number(const char *serial) {
// Filter watches known to be misaligned based on the serial number. This is possible because
// Serial numbers are represented as strings as described in:
// https://pebbletechnology.atlassian.net/wiki/display/DEV/Hardware+Serial+Numbering
// All watches of the same model produced from the same manufacturer on the same date, on the
// same manufacturing line, will share the same first 8 characters of the serial number. In this
// way, batches which are misaligned can be identified by a string comparison on these characters.
//
// NOTE: This also conveniently excludes test automation boards, so the dialog should not
// appear during integration tests.
const char *ranges[] = { "Q402445E" };
for (size_t i = 0; i < ARRAY_LENGTH(ranges); i++) {
if (strncmp(serial, ranges[i], strlen(ranges[i])) == 0) {
return true;
}
}
return false;
}
static bool prv_is_potentially_misaligned_watch() {
return !prv_display_has_user_offset() &&
prv_is_known_misaligned_serial_number(mfg_get_serial_number());
}
void display_calibration_prompt_show_if_needed(void) {
if (!prv_is_potentially_misaligned_watch()) {
shell_prefs_set_should_prompt_display_calibration(false);
return;
}
if (shell_prefs_should_prompt_display_calibration()) {
s_timer = new_timer_create();
const uint32_t prompt_delay_time_ms = MS_PER_SECOND * SECONDS_PER_MINUTE;
new_timer_start(s_timer, prompt_delay_time_ms, prv_timer_callback, NULL, 0 /* flags */);
}
}
#endif

View File

@@ -0,0 +1,19 @@
/*
* 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.
*/
#pragma once
void display_calibration_prompt_show_if_needed(void);

View File

@@ -0,0 +1,43 @@
/*
* 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 "language_ui.h"
#include <stdio.h>
#include "applib/ui/dialogs/simple_dialog.h"
#include "kernel/event_loop.h"
#include "kernel/ui/modals/modal_manager.h"
#include "resource/resource_ids.auto.h"
#include "services/common/i18n/i18n.h"
#include "shell/normal/watchface.h"
static void prv_push_language_changed_dialog(void *data) {
const char *lang_name = (const char *)data;
SimpleDialog *simple_dialog = simple_dialog_create("LangFileChanged");
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
dialog_set_text(dialog, lang_name);
dialog_set_icon(dialog, RESOURCE_ID_GENERIC_CONFIRMATION_LARGE);
dialog_set_background_color(dialog, GColorJaegerGreen);
dialog_set_timeout(dialog, DIALOG_TIMEOUT_DEFAULT);
simple_dialog_push(simple_dialog, modal_manager_get_window_stack(ModalPriorityAlert));
// after dialog closes, launch the watchface
watchface_launch_default(NULL);
}
void language_ui_display_changed(const char *lang_name) {
launcher_task_add_callback(prv_push_language_changed_dialog, (void *)lang_name);
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
#pragma once
// Show a dialog indicating the language has changed
void language_ui_display_changed(const char *lang_name);

1178
src/fw/shell/normal/prefs.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
// Included by prefs.c to declare the list of preferences and their keys
PREFS_MACRO(PREF_KEY_CLOCK_24H, s_clock_24h)
PREFS_MACRO(PREF_KEY_CLOCK_TIMEZONE_SOURCE_IS_MANUAL, s_clock_timezone_source_is_manual)
PREFS_MACRO(PREF_KEY_CLOCK_PHONE_TIMEZONE_ID, s_clock_phone_timezone_id)
PREFS_MACRO(PREF_KEY_UNITS_DISTANCE, s_units_distance)
PREFS_MACRO(PREF_KEY_BACKLIGHT_ENABLED, s_backlight_enabled)
PREFS_MACRO(PREF_KEY_BACKLIGHT_AMBIENT_SENSOR_ENABLED, s_backlight_ambient_sensor_enabled)
PREFS_MACRO(PREF_KEY_BACKLIGHT_TIMEOUT_MS, s_backlight_timeout_ms)
PREFS_MACRO(PREF_KEY_BACKLIGHT_INTENSITY, s_backlight_intensity)
PREFS_MACRO(PREF_KEY_BACKLIGHT_MOTION, s_backlight_motion_enabled)
PREFS_MACRO(PREF_KEY_STATIONARY, s_stationary_mode_enabled)
PREFS_MACRO(PREF_KEY_DEFAULT_WORKER, s_default_worker)
PREFS_MACRO(PREF_KEY_TEXT_STYLE, s_text_style)
PREFS_MACRO(PREF_KEY_LANG_ENGLISH, s_language_english)
PREFS_MACRO(PREF_KEY_QUICK_LAUNCH_UP, s_quick_launch_up)
PREFS_MACRO(PREF_KEY_QUICK_LAUNCH_DOWN, s_quick_launch_down)
PREFS_MACRO(PREF_KEY_QUICK_LAUNCH_SELECT, s_quick_launch_select)
PREFS_MACRO(PREF_KEY_QUICK_LAUNCH_BACK, s_quick_launch_back)
PREFS_MACRO(PREF_KEY_QUICK_LAUNCH_SETUP_OPENED, s_quick_launch_setup_opened)
PREFS_MACRO(PREF_KEY_DEFAULT_WATCHFACE, s_default_watchface)
PREFS_MACRO(PREF_KEY_WELCOME_VERSION, s_welcome_version)
#if CAPABILITY_HAS_HEALTH_TRACKING
PREFS_MACRO(PREF_KEY_ACTIVITY_PREFERENCES, s_activity_preferences)
PREFS_MACRO(PREF_KEY_ACTIVITY_ACTIVATED_TIMESTAMP, s_activity_activation_timestamp)
PREFS_MACRO(PREF_KEY_ACTIVITY_ACTIVATION_DELAY_INSIGHT, s_activity_activation_delay_insight)
PREFS_MACRO(PREF_KEY_ACTIVITY_HEALTH_APP_OPENED, s_activity_prefs_health_app_opened)
PREFS_MACRO(PREF_KEY_ACTIVITY_WORKOUT_APP_OPENED, s_activity_prefs_workout_app_opened)
PREFS_MACRO(PREF_KEY_ALARMS_APP_OPENED, s_alarms_app_opened)
PREFS_MACRO(PREF_KEY_ACTIVITY_HRM_PREFERENCES, s_activity_hrm_preferences)
PREFS_MACRO(PREF_KEY_ACTIVITY_HEART_RATE_PREFERENCES, s_activity_hr_preferences)
#endif
#if PLATFORM_SPALDING
PREFS_MACRO(PREF_KEY_DISPLAY_USER_OFFSET, s_display_user_offset)
PREFS_MACRO(PREF_KEY_SHOULD_PROMPT_DISPLAY_CALIBRATION, s_should_prompt_display_calibration)
#endif
#if CAPABILITY_HAS_TIMELINE_PEEK
PREFS_MACRO(PREF_KEY_TIMELINE_SETTINGS_OPENED, s_timeline_settings_opened)
PREFS_MACRO(PREF_KEY_TIMELINE_PEEK_ENABLED, s_timeline_peek_enabled)
PREFS_MACRO(PREF_KEY_TIMELINE_PEEK_BEFORE_TIME_M, s_timeline_peek_before_time_m)
#endif

View File

@@ -0,0 +1,28 @@
/*
* 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.
*/
#pragma once
#include "drivers/button_id.h"
#include "process_management/app_install_types.h"
#include "util/uuid.h"
bool quick_launch_is_enabled(ButtonId button);
AppInstallId quick_launch_get_app(ButtonId button);
void quick_launch_set_app(ButtonId button, AppInstallId app_id);
void quick_launch_set_enabled(ButtonId button, bool enabled);
void quick_launch_set_quick_launch_setup_opened(uint8_t version);
uint8_t quick_launch_get_quick_launch_setup_opened(void);

125
src/fw/shell/normal/shell.c Normal file
View File

@@ -0,0 +1,125 @@
/*
* 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 "shell/shell.h"
#include "apps/system_app_ids.h"
#include "kernel/pbl_malloc.h"
#include "process_management/app_install_manager.h"
#include "process_management/app_install_types.h"
#include "process_management/app_manager.h"
#include "services/common/compositor/compositor_transitions.h"
#define WATCHFACE_SHUTTER_COLOR GColorWhite
#define HEALTH_SHUTTER_COLOR PBL_IF_COLOR_ELSE(GColorBlack, GColorWhite)
#define ACTION_SHUTTER_COLOR PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite)
static const CompositorTransition *prv_get_watchface_compositor_animation(
CompositorTransitionDirection direction) {
return PBL_IF_RECT_ELSE(compositor_shutter_transition_get(direction, WATCHFACE_SHUTTER_COLOR),
compositor_port_hole_transition_app_get(direction));
}
static const CompositorTransition *prv_get_health_compositor_animation(
CompositorTransitionDirection direction) {
return PBL_IF_RECT_ELSE(compositor_shutter_transition_get(direction, HEALTH_SHUTTER_COLOR),
compositor_port_hole_transition_app_get(direction));
}
static const CompositorTransition *prv_get_action_compositor_animation(
CompositorTransitionDirection direction) {
return PBL_IF_RECT_ELSE(compositor_shutter_transition_get(direction, ACTION_SHUTTER_COLOR),
NULL);
}
const CompositorTransition *shell_get_watchface_compositor_animation(
bool watchface_is_destination) {
const CompositorTransitionDirection direction = watchface_is_destination ?
CompositorTransitionDirectionLeft : CompositorTransitionDirectionRight;
return prv_get_watchface_compositor_animation(direction);
}
static const CompositorTransition *prv_app_launcher_transition_animation(
CompositorTransitionDirection direction) {
const bool app_is_destination = (direction == CompositorTransitionDirectionRight);
return PBL_IF_RECT_ELSE(compositor_launcher_app_transition_get(app_is_destination),
compositor_port_hole_transition_app_get(direction));
}
const CompositorTransition *shell_get_close_compositor_animation(AppInstallId current_app_id,
AppInstallId next_app_id) {
const CompositorTransition *res = NULL;
AppInstallEntry *app_entry = kernel_zalloc_check(sizeof(AppInstallEntry));
if (app_install_get_entry_for_install_id(next_app_id, app_entry) &&
app_install_entry_is_watchface(app_entry)) {
if (current_app_id == APP_ID_LAUNCHER_MENU) {
res = prv_get_watchface_compositor_animation(CompositorTransitionDirectionLeft);
goto done;
} else if (current_app_id == APP_ID_HEALTH_APP) {
res = prv_get_health_compositor_animation(CompositorTransitionDirectionDown);
goto done;
} else {
res = prv_get_action_compositor_animation(CompositorTransitionDirectionLeft);
goto done;
}
}
if (next_app_id == APP_ID_LAUNCHER_MENU) {
res = prv_app_launcher_transition_animation(CompositorTransitionDirectionLeft);
goto done;
}
// If we get here, we don't use a compositor animation for the transition
done:
kernel_free(app_entry);
return res;
}
const CompositorTransition *shell_get_open_compositor_animation(AppInstallId current_app_id,
AppInstallId next_app_id) {
const CompositorTransition *res = NULL;
AppInstallEntry *app_entry = kernel_zalloc_check(sizeof(AppInstallEntry));
if (app_install_get_entry_for_install_id(current_app_id, app_entry)) {
if (app_install_entry_is_watchface(app_entry)) {
if (next_app_id == APP_ID_LAUNCHER_MENU) {
res = prv_get_watchface_compositor_animation(CompositorTransitionDirectionRight);
goto done;
} else if (next_app_id == APP_ID_HEALTH_APP) {
res = prv_get_health_compositor_animation(CompositorTransitionDirectionUp);
goto done;
}
} else if ((current_app_id == APP_ID_HEALTH_APP) &&
app_install_get_entry_for_install_id(next_app_id, app_entry) &&
app_install_entry_is_watchface(app_entry)) {
res = prv_get_health_compositor_animation(CompositorTransitionDirectionDown);
goto done;
}
}
if (current_app_id == APP_ID_LAUNCHER_MENU) {
res = prv_app_launcher_transition_animation(CompositorTransitionDirectionRight);
goto done;
}
// If we get here, we don't use a compositor animation for the transition
done:
kernel_free(app_entry);
return res;
}

View File

@@ -0,0 +1,204 @@
/*
* 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 <kernel/events.h>
#include "shell/shell_event_loop.h"
#include "shell/prefs_private.h"
#include "apps/system_app_ids.h"
#include "apps/system_apps/app_fetch_ui.h"
#include "apps/system_apps/settings/settings_quick_launch.h"
#include "apps/system_apps/timeline/timeline.h"
#include "kernel/low_power.h"
#include "kernel/pbl_malloc.h"
#include "popups/alarm_popup.h"
#include "popups/bluetooth_pairing_ui.h"
#include "popups/notifications/notification_window.h"
#include "popups/timeline/peek.h"
#include "process_management/app_install_manager.h"
#include "process_management/app_manager.h"
#include "process_management/process_manager.h"
#include "services/common/analytics/analytics.h"
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
#include "services/common/shared_prf_storage/shared_prf_storage.h"
#include "services/normal/activity/activity.h"
#include "services/normal/activity/workout_service.h"
#include "services/normal/app_inbox_service.h"
#include "services/normal/app_outbox_service.h"
#include "services/normal/music.h"
#include "services/normal/music_endpoint.h"
#include "services/normal/notifications/do_not_disturb.h"
#include "services/normal/stationary.h"
#include "services/normal/timeline/event.h"
#include "shell/normal/app_idle_timeout.h"
#include "shell/normal/battery_ui.h"
#include "shell/normal/display_calibration_prompt.h"
#include "shell/normal/quick_launch.h"
#include "shell/normal/watchface.h"
#include "shell/normal/welcome.h"
#include "shell/prefs.h"
#include "system/logging.h"
extern void shell_prefs_init(void);
void shell_event_loop_init(void) {
shell_prefs_init();
#if PLATFORM_SPALDING
shell_prefs_display_offset_init();
display_calibration_prompt_show_if_needed();
#endif
notification_window_service_init();
app_inbox_service_init();
app_outbox_service_init();
app_message_sender_init();
watchface_init();
timeline_peek_init();
#if CAPABILITY_HAS_HEALTH_TRACKING
// Start activity tracking if enabled
if (activity_prefs_tracking_is_enabled()) {
activity_start_tracking(false /*test_mode*/);
}
workout_service_init();
#endif
bool factory_reset_or_first_use = !shared_prf_storage_get_getting_started_complete();
// We are almost done booting, welcome the user if applicable. This _must_ occur before setting
// the getting started completed below.
welcome_push_notification(factory_reset_or_first_use);
if (factory_reset_or_first_use) {
bt_persistent_storage_set_unfaithful(true);
}
// As soon as we boot normally for the first time, we've therefore completed first use mode and
// we don't need to go through it again until we factory reset.
shared_prf_storage_set_getting_started_complete(true /* complete */);
}
void shell_event_loop_handle_event(PebbleEvent *e) {
switch (e->type) {
case PEBBLE_APP_FETCH_REQUEST_EVENT:
app_manager_handle_app_fetch_request_event(&e->app_fetch_request);
return;
case PEBBLE_ALARM_CLOCK_EVENT:
analytics_inc(ANALYTICS_DEVICE_METRIC_ALARM_SOUNDED_COUNT, AnalyticsClient_System);
PBL_LOG(LOG_LEVEL_INFO, "Alarm event in the shell event loop");
stationary_wake_up();
alarm_popup_push_window(&e->alarm_clock);
return;
case PEBBLE_BT_PAIRING_EVENT:
bluetooth_pairing_ui_handle_event(&e->bluetooth.pair);
return;
case PEBBLE_APP_WILL_CHANGE_FOCUS_EVENT:
if (e->app_focus.in_focus) {
app_idle_timeout_resume();
} else {
app_idle_timeout_pause();
}
return;
case PEBBLE_SYS_NOTIFICATION_EVENT:
// This handles incoming Notifications and actions on Notifications and Reminders
notification_window_handle_notification(&e->sys_notification);
return;
case PEBBLE_CALENDAR_EVENT:
do_not_disturb_handle_calendar_event(&e->calendar);
return;
case PEBBLE_TIMELINE_PEEK_EVENT:
timeline_peek_handle_peek_event(&e->timeline_peek);
return;
case PEBBLE_BLOBDB_EVENT:
{
// Calendar should only handle pin_db events
PebbleBlobDBEvent *blobdb_event = &e->blob_db;
if (blobdb_event->db_id == BlobDBIdPins) {
timeline_event_handle_blobdb_event();
} else if (blobdb_event->db_id == BlobDBIdPrefs) {
prefs_private_handle_blob_db_event(blobdb_event);
}
return;
}
case PEBBLE_DO_NOT_DISTURB_EVENT:
notification_window_handle_dnd_event(&e->do_not_disturb);
return;
case PEBBLE_REMINDER_EVENT:
// This handles incoming Reminders
notification_window_handle_reminder(&e->reminder);
return;
case PEBBLE_BATTERY_STATE_CHANGE_EVENT:
battery_ui_handle_state_change_event(e->battery_state.new_state);
return;
case PEBBLE_COMM_SESSION_EVENT:
music_endpoint_handle_mobile_app_event(&e->bluetooth.comm_session_event);
return;
// Sent by the comm layer once we get a response from the mobile app to a phone version request
case PEBBLE_REMOTE_APP_INFO_EVENT:
music_endpoint_handle_mobile_app_info_event(&e->bluetooth.app_info_event);
analytics_inc(ANALYTICS_DEVICE_METRIC_PHONE_APP_INFO_COUNT, AnalyticsClient_System);
return;
case PEBBLE_MEDIA_EVENT:
if (e->media.playback_state == MusicPlayStatePlaying) {
app_install_mark_prioritized(APP_ID_MUSIC, true /* can_expire */);
}
return;
case PEBBLE_HEALTH_SERVICE_EVENT:
workout_service_health_event_handler(&e->health_event);
return;
case PEBBLE_ACTIVITY_EVENT:
workout_service_activity_event_handler(&e->activity_event);
return;
case PEBBLE_WORKOUT_EVENT: {
// If a workout is ongoing, keep the app at the top of the launcher.
// When a workout is stopped it will return to it's normal position after the
// default timeout.
PebbleWorkoutEvent *workout_e = &e->workout;
bool can_expire = true;
switch (workout_e->type) {
case PebbleWorkoutEvent_Started:
case PebbleWorkoutEvent_Paused:
can_expire = false;
break;
case PebbleWorkoutEvent_Stopped:
can_expire = true;
break;
case PebbleWorkoutEvent_FrontendOpened:
case PebbleWorkoutEvent_FrontendClosed:
break;
}
app_install_mark_prioritized(APP_ID_WORKOUT, can_expire);
workout_service_workout_event_handler(workout_e);
return;
}
default:
break; // don't care
}
}

View File

@@ -0,0 +1,172 @@
/*
* 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 <inttypes.h>
#include <stdio.h>
#include <string.h>
#include "applib/app.h"
#include "applib/app_timer.h"
#include "applib/battery_state_service.h"
#include "applib/ui/dialogs/simple_dialog.h"
#include "applib/ui/window.h"
#include "kernel/pbl_malloc.h"
#include "kernel/util/standby.h"
#include "process_management/pebble_process_md.h"
#include "process_management/worker_manager.h"
#include "process_state/app_state/app_state.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "services/runlevel.h"
#include "services/common/status_led.h"
#include "system/logging.h"
#include "system/passert.h"
#include "system/reboot_reason.h"
#include "system/reset.h"
static const uint32_t CHARGER_DISCONNECT_TIMEOUT_MS = 3000;
typedef enum DialogState {
DialogState_Uninitialized = 0,
DialogState_Charging,
DialogState_FullyCharged,
} DialogState;
struct AppData {
SimpleDialog *dialog;
AppTimer *poweroff_timer;
DialogState last_dialog_state;
bool was_plugged;
};
static void prv_reboot_on_click(ClickRecognizerRef recognizer, void *data) {
// Don't try to return to normal functioning; just reboot the watch. The user
// thinks the watch is already off anyway.
RebootReason reboot_reason = { RebootReasonCode_ShutdownMenuItem };
reboot_reason_set(&reboot_reason);
system_reset();
}
static void prv_config_provider(void *context) {
for (int i = 0; i < NUM_BUTTONS; ++i) {
window_long_click_subscribe(i, 0, prv_reboot_on_click, NULL);
}
}
static void prv_power_off_timer_expired(void *data) {
enter_standby(RebootReasonCode_ShutdownMenuItem);
}
static void prv_battery_state_handler(BatteryChargeState charge) {
struct AppData *data = app_state_get_user_data();
if (charge.is_plugged && !data->was_plugged) {
app_timer_cancel(data->poweroff_timer);
} else if (!charge.is_plugged && data->was_plugged) {
data->poweroff_timer = app_timer_register(
CHARGER_DISCONNECT_TIMEOUT_MS, prv_power_off_timer_expired, NULL);
}
DialogState next_dialog_state = DialogState_Uninitialized;
if (charge.is_charging) {
next_dialog_state = DialogState_Charging;
} else if (charge.is_plugged) {
next_dialog_state = DialogState_FullyCharged;
} else {
// Unplugged. We'll be shutting down in a couple seconds if the user doesn't
// plug the charger back in, so don't change the dialog.
next_dialog_state = data->last_dialog_state;
}
Dialog *dialog = simple_dialog_get_dialog(data->dialog);
if (next_dialog_state != data->last_dialog_state) {
// Setting the dialog icon to itself restarts the animation, which looks
// bad, so we want to avoid that if we can help it.
switch (next_dialog_state) {
case DialogState_FullyCharged:
dialog_set_text(dialog, i18n_get("Fully Charged", data));
dialog_set_icon(dialog, RESOURCE_ID_BATTERY_ICON_FULL_LARGE_INVERTED);
break;
case DialogState_Charging:
default:
dialog_set_text(dialog, i18n_get("Charging", data));
dialog_set_icon(dialog, RESOURCE_ID_BATTERY_ICON_CHARGING_LARGE_INVERTED);
break;
}
}
if (charge.is_plugged) {
if (charge.is_charging) {
status_led_set(StatusLedState_Charging);
} else {
status_led_set(StatusLedState_FullyCharged);
}
} else {
status_led_set(StatusLedState_Off);
}
data->was_plugged = charge.is_plugged;
data->last_dialog_state = next_dialog_state;
}
static void prv_handle_init(void) {
struct AppData *data = app_malloc_check(sizeof(struct AppData));
*data = (struct AppData){};
app_state_set_user_data(data);
data->dialog = simple_dialog_create(WINDOW_NAME("Shutdown Charging"));
Dialog *dialog = simple_dialog_get_dialog(data->dialog);
dialog_set_background_color(dialog, GColorBlack);
dialog_set_text_color(dialog, GColorWhite);
window_set_click_config_provider(&dialog->window, prv_config_provider);
// The assumption is that this app is launched when the charger is connected
// and the shutdown menu item is selected.
data->was_plugged = true;
data->last_dialog_state = DialogState_Uninitialized;
battery_state_service_subscribe(prv_battery_state_handler);
// Handle the edge-case where the charger is disconnected between the user
// selecting shut down and this app subscribing to battery state events.
// Also set the initial battery charge level.
prv_battery_state_handler(battery_state_service_peek());
app_simple_dialog_push(data->dialog);
// TODO: have the runlevel machinery disable bluetooth and worker.
services_set_runlevel(RunLevel_BareMinimum);
worker_manager_disable();
}
static void s_main(void) {
prv_handle_init();
app_event_loop();
}
const PebbleProcessMd* shutdown_charging_get_app_info(void) {
static const PebbleProcessMdSystem s_app_md = {
.common = {
.main_func = s_main,
.visibility = ProcessVisibilityHidden,
// UUID: 48fa66c4-4e6f-4b32-bf75-a16e12d630c3
.uuid = {0x48, 0xfa, 0x66, 0xc4, 0x4e, 0x6f, 0x4b, 0x32,
0xbf, 0x75, 0xa1, 0x6e, 0x12, 0xd6, 0x30, 0xc3},
},
.name = "Shutdown Charging",
};
return (const PebbleProcessMd*) &s_app_md;
}

View File

@@ -0,0 +1,621 @@
{
"warning_one": [
" 1. DO NOT CHANGE OR REUSE THE ID OF ANY APPLICATION IN THE LIST ",
" 2. Read the directions "
],
"directions": [
" - The System Apps are the applications that are actually coded",
" into the firmware with static PebbleProcessMd's. ",
" - The Resource Apps are the applications that are stored in ",
" the resource pack included with the firmware. ",
" - The section 'system_apps' only lists the PebbleProcessMd* ",
" functions ",
" - Resource app entry requires a UUID, then bin_resource_id ",
" then an icon_resource_id ",
" - To enable only certain applications on a certain ",
" add the particular DEFINE variable into the 'ifdefs' field ",
" Doing so will add that application to the generated list ",
" - To disable applications entirely, add a 'DISABLED' define to",
" the list of defines for the application "
],
"warning_two": [
" 1. DO NOT CHANGE OR REUSE THE ID OF ANY APPLICATION IN THE LIST ",
" 2. Read the directions "
],
"system_apps": [
{
"id": -69,
"enum": "TICTOC",
"md_fn": "tictoc_get_app_info"
},
{
"id": -98,
"enum": "KICKSTART",
"md_fn": "kickstart_get_app_info",
"target_platforms": [
"snowy",
"spalding",
"silk",
"robert"
]
},
{
"id": -2,
"enum": "LOW_POWER_FACE",
"md_fn": "low_power_face_get_app_info"
},
{
"id": -7,
"enum": "SETTINGS",
"md_fn": "settings_get_app_info",
"color_argb8": "GColorLightGrayARGB8"
},
{
"id": -3,
"enum": "MUSIC",
"md_fn": "music_app_get_info",
"color_argb8": "GColorOrangeARGB8"
},
{
"id": -4,
"enum": "NOTIFICATIONS",
"md_fn": "notifications_app_get_info",
"color_argb8": "GColorSunsetOrangeARGB8"
},
{
"id": -5,
"enum": "ALARMS",
"md_fn": "alarms_app_get_info",
"color_argb8": "GColorJaegerGreenARGB8"
},
{
"id": -6,
"enum": "WATCHFACES",
"md_fn": "watchfaces_get_app_info",
"color_argb8": "GColorJazzberryJamARGB8"
},
{
"id": -9,
"enum": "QUICK_LAUNCH_SETUP",
"md_fn": "quick_launch_setup_get_app_info"
},
{
"id": -10,
"enum": "TIMELINE",
"md_fn": "timeline_get_app_info"
},
{
"id": -12,
"enum": "BOUNCING_BOX_DEMO",
"md_fn": "bouncing_box_demo_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"],
"target_platforms": [
"snowy",
"robert"
]
},
{
"id": -13,
"enum": "PEBBLE_COLORS",
"md_fn": "pebble_colors_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -14,
"enum": "PEBBLE_SHAPES",
"md_fn": "pebble_shapes_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -17,
"enum": "MOVABLE_LINE",
"md_fn": "movable_line_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -18,
"enum": "GFX_TESTS",
"md_fn": "gfx_tests_get_app_info",
"ifdefs": ["PERFORMANCE_TESTS"]
},
{
"id": -19,
"enum": "TEST_ARGS_RECEIVER",
"md_fn": "test_args_receiver_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -20,
"enum": "TEST_ARGS_SENDER",
"md_fn": "test_args_sender_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -21,
"enum": "FLASH_DIAGNOSTIC",
"md_fn": "flash_diagnostic_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -22,
"enum": "FS_RESOURCES",
"md_fn": "fs_resources_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -23,
"enum": "EXIT",
"md_fn": "exit_app_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -25,
"enum": "APP_HEAP_DEMO",
"md_fn": "app_heap_demo_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -26,
"enum": "DATA_LOGGING_TEST",
"md_fn": "data_logging_test_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -27,
"enum": "KILL_BT",
"md_fn": "kill_bt_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -28,
"enum": "TRIGGER_ALARM",
"md_fn": "trigger_alarm_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -29,
"enum": "ANIMATED_DEMO",
"md_fn": "animated_demo_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -30,
"enum": "GRENADE_LAUNCHER",
"md_fn": "grenade_launcher_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -31,
"enum": "TEXT_LAYOUT",
"md_fn": "text_layout_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -33,
"enum": "MENU",
"md_fn": "menu_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -34,
"enum": "SIMPLE_MENU",
"md_fn": "simple_menu_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -35,
"enum": "SCROLL",
"md_fn": "scroll_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -36,
"enum": "CLICK",
"md_fn": "click_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -37,
"enum": "PROGRESS",
"md_fn": "progress_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -38,
"enum": "NUMBER_FIELD",
"md_fn": "number_field_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -41,
"enum": "EVENT_SERVICE",
"md_fn": "event_service_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -43,
"enum": "PERSIST",
"md_fn": "persist_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -44,
"enum": "TIMER",
"md_fn": "timer_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -45,
"enum": "TEST_SYS_TIMER",
"md_fn": "test_sys_timer_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -46,
"enum": "TEST_CORE_DUMP",
"md_fn": "test_core_dump_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -47,
"enum": "FLASH_PROF",
"md_fn": "flash_prof_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -48,
"enum": "TEST_BLUETOOTH",
"md_fn": "test_bluetooth_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -50,
"enum": "VIBE_AND_LOGS",
"md_fn": "vibe_and_logs_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -51,
"enum": "MENU_OVERFLOW",
"md_fn": "menu_overflow_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -54,
"enum": "LAUNCHER_MENU",
"md_fn": "launcher_menu_app_get_app_info"
},
{
"id": -55,
"enum": "TEXT_CLIPPING",
"md_fn": "text_clipping_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -56,
"enum": "LIGHT_CONFIG",
"md_fn": "light_config_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -57,
"enum": "STROKE_WIDTH",
"md_fn": "stroke_width_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"],
"target_platforms": [
"snowy"
]
},
{
"id": -58,
"enum": "AMB_LIGHT_READ",
"md_fn": "ambient_light_reading_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -59,
"enum": "WEATHER",
"md_fn": "weather_app_get_info",
"color_argb8": "GColorBlueMoonARGB8",
"target_platforms":[
"snowy",
"spalding",
"silk",
"robert"
]
},
{
"id": -95,
"enum": "WORKOUT",
"md_fn": "workout_app_get_info",
"color_argb8": "GColorYellowARGB8",
"target_platforms": [
"silk"
]
},
{
"id": -60,
"enum": "SHUTDOWN_CHARGING",
"md_fn": "shutdown_charging_get_app_info",
"target_platforms": [
"snowy",
"spalding",
"robert"
]
},
{
"id": -61,
"enum": "SWAP_LAYER",
"md_fn": "swap_layer_demo_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -62,
"enum": "BATTERY_CRITICAL",
"md_fn": "battery_critical_get_app_info"
},
{
"id": -63,
"enum": "TEXT_SPACING",
"md_fn": "text_spacing_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -64,
"enum": "KINO_LAYER",
"md_fn": "kino_layer_demo_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -65,
"enum": "DIALOGS",
"md_fn": "dialogs_demo_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -66,
"enum": "STATUSBAR",
"md_fn": "statusbar_demo_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -68,
"enum": "MORPH_SQUARE",
"md_fn": "morph_square_demo_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -70,
"enum": "MENU_RIGHT_ICON",
"md_fn": "menu_layer_right_icon_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -71,
"enum": "VIBE_STRENGTH",
"md_fn": "vibe_strength_demo_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -72,
"enum": "ACTION_MENU",
"md_fn": "action_menu_demo_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -73,
"enum": "OPTION_MENU",
"md_fn": "option_menu_demo_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -74,
"enum": "PROFILE_MUTEXES",
"md_fn": "profile_mutexes_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS", "DISABLED"]
},
{
"id": -75,
"enum": "DEADLOCK",
"md_fn": "deadlock_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -76,
"enum": "TIMELINE_PINS",
"md_fn": "timeline_pins_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -77,
"enum": "TEXT_FLOW",
"md_fn": "text_flow_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -78,
"enum": "MENU_ROUND",
"md_fn": "menu_round_app_get_info",
"ifdefs": ["ENABLE_TEST_APPS"],
"target_platforms": [
"spalding"
]
},
{
"id": -79,
"enum": "ACTIVITY_DEMO",
"md_fn": "activity_demo_get_app_info",
"ifdefs": ["SHOW_ACTIVITY_DEMO"],
"target_platforms": [
"snowy",
"spalding",
"silk",
"robert"
]
},
{
"id": -80,
"enum": "ACTIVITY_TEST",
"md_fn": "activity_test_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"],
"target_platforms": [
"snowy",
"spalding",
"silk",
"robert"
]
},
{
"id": -81,
"enum": "DOUBLE_TAP_TEST",
"md_fn": "double_tap_test_get_info",
"ifdefs": ["ENABLE_TEST_APPS"]
},
{
"id": -82,
"enum": "HEALTH_APP",
"md_fn": "health_app_get_info",
"color_argb8": "GColorSunsetOrangeARGB8",
"target_platforms": [
"snowy",
"spalding",
"silk",
"robert"
]
},
{
"id": -83,
"enum": "SEND_TEXT",
"md_fn": "send_text_app_get_info",
"target_platforms": [
"snowy",
"spalding",
"silk",
"robert"
]
},
{
"id": -84,
"enum": "VIBE_SCORE",
"md_fn": "vibe_score_demo_get_info",
"ifdefs": ["ENABLE_TEST_APPS"],
"target_platforms": [
"snowy",
"spalding"
]
},
{
"id": -86,
"enum": "IDL",
"md_fn": "idl_demo_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"],
"target_platforms": [
"snowy",
"spalding"
]
},
{
"id": -87,
"enum": "TEMPERATURE_DEMO",
"md_fn": "temperature_demo_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"],
"target_platforms": [
"snowy"
]
},
{
"id": -88,
"enum": "GDRAWMASK_DEMO",
"md_fn": "gdrawmask_demo_get_app_info",
"ifdefs": ["ENABLE_TEST_APPS"],
"target_platforms": [
"snowy",
"spalding"
]
},
{
"id": -90,
"enum": "REMINDERS",
"md_fn": "reminder_app_get_info",
"color_argb8": "GColorChromeYellowARGB8",
"target_platforms": [
"snowy",
"spalding",
"silk",
"robert"
],
"ifdefs": ["CAPABILITY_HAS_MICROPHONE=1"]
},
{
"id": -91,
"enum": "MPU_TEST",
"md_fn": "test_mpu_cache_get_info",
"ifdefs": ["ENABLE_TEST_APPS"],
"target_platforms": [
"snowy",
"spalding",
"silk",
"robert"
]
},
{
"id": -92,
"enum": "QUIET_TIME_TOGGLE",
"md_fn": "quiet_time_toggle_get_app_info",
"target_platforms": [
"snowy",
"spalding",
"silk",
"robert"
]
},
{
"id": -94,
"enum": "MOTION_BACKLIGHT_TOGGLE",
"md_fn": "motion_backlight_toggle_get_app_info",
"target_platforms": [
"snowy",
"spalding",
"silk",
"robert"
]
},
{
"id": -93,
"enum": "AIRPLANE_MODE_TOGGLE",
"md_fn": "airplane_mode_toggle_get_app_info",
"target_platforms": [
"snowy",
"spalding",
"silk",
"robert"
]
},
{
"id": -96,
"enum": "TIMELINE_PAST",
"md_fn": "timeline_past_get_app_info",
"target_platforms": [
"snowy",
"spalding",
"silk",
"robert"
]
},
{
"id": -97,
"enum": "SPORTS",
"md_fn": "sports_app_get_info"
}
],
"resource_apps": [
{
"id": -52,
"enum": "GOLF",
"name": "Golf",
"uuid": "cf1e816a-9db0-4511-bbb8-f60c48ca8fac",
"bin_resource_id": "RESOURCE_ID_STORED_APP_GOLF",
"icon_resource_id": "DEFAULT_MENU_ICON"
}
]
}

View File

@@ -0,0 +1,133 @@
/*
* 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 "shell/system_app_state_machine.h"
#include "apps/core_apps/panic_window_app.h"
#include "apps/system_apps/battery_critical_app.h"
#include "apps/system_app_ids.h"
#include "apps/system_apps/launcher/launcher_app.h"
#include "apps/watch/low_power/low_power_face.h"
#include "shell/normal/watchface.h"
#include "kernel/low_power.h"
#include "kernel/panic.h"
#include "resource/resource.h"
#include "services/common/battery/battery_monitor.h"
#include "system/bootbits.h"
#include "system/logging.h"
#include "process_management/app_manager.h"
//! @file system_app_state_machine.c
//!
//! This file implements our app to app flow that makes up our normal shell. It defines
//! which app first runs at start up and what app should be launched to replace the current
//! app if the current app wants to close.
//!
//! The logic for which app should replace closing apps is a little tricky. Apps can be launched
//! in various ways, either due to direct user interaction (selecting an app in the launcher) or
//! through the phone app using pebble protocol (for example, a new app being installed or a
//! companion app launching its watchapp in response to an event). What we want to happen is
//! the user can then close that app and end up in a rough approximation of where they came from.
//!
//! The way we implement this is by having two apps that make up roots of the graph. If you're
//! in the launcher and you launch an app, closing that app will return to the launcher. If you
//! attempt to nest further (you launch an app from the launcher and that app in turn launches
//! another app), closing any app will still return you to the launcher. This is done to prevent
//! the stack from growing too deep and having to exit a ton of apps to get back to where you want.
//! The watchface is also a root (closing an app that launched while you were in a watchface
//! will return to you to the watchface). Finally, closing the launcher will return you to the
//! watchface, and closing the watchface (either by pressing select or the watchface crashing)
//! should take you to the launcher.
//!
//! Launching any watchface for any reason will put you in the "root watchface" state.
//!
//! Below is a pretty ASCII picture to describe the states we can be in. What happens when you
//! close an app is illustrated with the arrow with the X.
//!
//! +---------------------+----+ +-------------------------+-----+
//! | Remote Launched App | | | Remote Launched App | |
//! +---------------+-----+ <--+ | Launcher Launched App | <---+
//! X +---------------+---------+
//! ^ | X
//! | v ^ |
//! | | v
//! +----+----------------+ +X-----> +------+------------------+
//! | Watchface | | Launcher |
//! +---------------------+ <-----X+ +-------------------------+
//!
//! As per the above block comment, are we currently rooted in the watchface stack or the
//! launcher stack?
static bool s_rooted_in_watchface = false;
const PebbleProcessMd* system_app_state_machine_system_start(void) {
// start critical battery app when necessary
if (battery_monitor_critical_lockout()) {
return battery_critical_get_app_info();
}
if (low_power_is_active()) {
return low_power_face_get_app_info();
}
if (launcher_panic_get_current_error() != 0) {
return panic_app_get_app_info();
}
return launcher_menu_app_get_app_info();
}
//! @return True if the currently running app is an installed watchface
static bool prv_current_app_is_watchface(void) {
return app_install_is_watchface(app_manager_get_current_app_id());
}
AppInstallId system_app_state_machine_get_last_registered_app(void) {
// If we're rooted in the watchface but we're not the watchface itself, or the launcher
// is closing, we should launch the watchface.
if ((s_rooted_in_watchface && !prv_current_app_is_watchface())
|| (app_manager_get_current_app_md() == launcher_menu_app_get_app_info())) {
return watchface_get_default_install_id();
}
return APP_ID_LAUNCHER_MENU;
}
const PebbleProcessMd* system_app_state_machine_get_default_app(void) {
return launcher_menu_app_get_app_info();
}
void system_app_state_machine_register_app_launch(AppInstallId app_id) {
if (app_id == APP_ID_LAUNCHER_MENU) {
s_rooted_in_watchface = false;
} else if (app_install_is_watchface(app_id)) {
s_rooted_in_watchface = true;
}
// Other app launches don't modify our root so just ignore them.
}
void system_app_state_machine_panic(void) {
if (app_manager_is_initialized()) {
app_manager_launch_new_app(&(AppLaunchConfig) {
.md = panic_app_get_app_info(),
});
}
// Else, just wait for the app_manager to initialize to show the panic app using
// system_app_state_machine_system_start().
}

View File

@@ -0,0 +1,202 @@
/*
* 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 "watchface.h"
#include "apps/system_app_ids.h"
#include "apps/system_apps/launcher/launcher_app.h"
#include "apps/system_apps/settings/settings_quick_launch.h"
#include "apps/system_apps/settings/settings_quick_launch_app_menu.h"
#include "apps/system_apps/settings/settings_quick_launch_setup_menu.h"
#include "apps/system_apps/timeline/timeline.h"
#include "apps/watch/low_power/low_power_face.h"
#include "kernel/event_loop.h"
#include "kernel/low_power.h"
#include "kernel/ui/modals/modal_manager.h"
#include "popups/timeline/peek.h"
#include "process_management/app_manager.h"
#include "process_management/pebble_process_md.h"
#include "services/common/analytics/analytics.h"
#include "services/common/compositor/compositor_transitions.h"
#include "services/normal/notifications/do_not_disturb.h"
#include "system/logging.h"
#include "system/passert.h"
#define QUICK_LAUNCH_HOLD_MS (400)
static ClickManager s_click_manager;
static bool prv_should_ignore_button_click(void) {
if (app_manager_get_task_context()->closing_state != ProcessRunState_Running) {
// Ignore if the app is not running (such as if it is in the process of closing)
return true;
}
if (low_power_is_active()) {
// If we're in low power mode we dont allow any interaction
return true;
}
return false;
}
static void prv_launch_app_via_button(AppLaunchEventConfig *config,
ClickRecognizerRef recognizer) {
config->common.button = click_recognizer_get_button_id(recognizer);
app_manager_put_launch_app_event(config);
}
static void prv_quick_launch_handler(ClickRecognizerRef recognizer, void *data) {
ButtonId button = click_recognizer_get_button_id(recognizer);
if (!quick_launch_is_enabled(button)) {
return;
}
AppInstallId app_id = quick_launch_get_app(button);
if (app_id == INSTALL_ID_INVALID) {
app_id = app_install_get_id_for_uuid(&quick_launch_setup_get_app_info()->uuid);
}
prv_launch_app_via_button(&(AppLaunchEventConfig) {
.id = app_id,
.common.reason = APP_LAUNCH_QUICK_LAUNCH,
}, recognizer);
}
static void prv_launch_timeline(ClickRecognizerRef recognizer, void *data) {
static TimelineArgs s_timeline_args;
const bool is_up = (click_recognizer_get_button_id(recognizer) == BUTTON_ID_UP);
if (is_up) {
PBL_LOG(LOG_LEVEL_DEBUG, "Launching timeline in past mode.");
s_timeline_args.direction = TimelineIterDirectionPast;
analytics_inc(ANALYTICS_DEVICE_METRIC_TIMELINE_PAST_LAUNCH_COUNT, AnalyticsClient_System);
} else {
PBL_LOG(LOG_LEVEL_DEBUG, "Launching timeline in future mode.");
s_timeline_args.direction = TimelineIterDirectionFuture;
analytics_inc(ANALYTICS_DEVICE_METRIC_TIMELINE_FUTURE_LAUNCH_COUNT, AnalyticsClient_System);
}
s_timeline_args.launch_into_pin = true;
s_timeline_args.stay_in_list_view = true;
timeline_peek_get_item_id(&s_timeline_args.pin_id);
const CompositorTransition *animation = NULL;
const bool is_future = (s_timeline_args.direction == TimelineIterDirectionFuture);
const bool timeline_is_destination = true;
#if PBL_ROUND
animation = compositor_dot_transition_timeline_get(is_future, timeline_is_destination);
#else
const bool jump = (!uuid_is_invalid(&s_timeline_args.pin_id) && !timeline_peek_is_first_event());
animation = jump ? compositor_peek_transition_timeline_get() :
compositor_slide_transition_timeline_get(is_future, timeline_is_destination,
timeline_peek_is_future_empty());
#endif
prv_launch_app_via_button(&(AppLaunchEventConfig) {
.id = APP_ID_TIMELINE,
.common.args = &s_timeline_args,
.common.transition = animation,
}, recognizer);
}
static void prv_configure_click_handler(ButtonId button_id, ClickHandler single_click_handler) {
ClickConfig *cfg = &s_click_manager.recognizers[button_id].config;
cfg->long_click.delay_ms = QUICK_LAUNCH_HOLD_MS;
cfg->long_click.handler = prv_quick_launch_handler;
cfg->click.handler = single_click_handler;
}
static void prv_launch_launcher_app(ClickRecognizerRef recognizer, void *data) {
static const LauncherMenuArgs s_launcher_args = { .reset_scroll = true };
prv_launch_app_via_button(&(AppLaunchEventConfig) {
.id = APP_ID_LAUNCHER_MENU,
.common.args = &s_launcher_args,
}, recognizer);
}
#if CAPABILITY_HAS_CORE_NAVIGATION4
static void prv_launch_health_app(ClickRecognizerRef recognizer, void *data) {
prv_launch_app_via_button(&(AppLaunchEventConfig) {
.id = APP_ID_HEALTH_APP,
}, recognizer);
}
#endif // CAPABILITY_HAS_CORE_NAVIGATION4
static ClickHandler prv_get_up_click_handler(void) {
#if CAPABILITY_HAS_CORE_NAVIGATION4
return prv_launch_health_app;
#else
return prv_launch_timeline;
#endif // CAPABILITY_HAS_CORE_NAVIGATION4
}
static void prv_dismiss_timeline_peek(ClickRecognizerRef recognizer, void *data) {
timeline_peek_dismiss();
}
static void prv_watchface_configure_click_handlers(void) {
prv_configure_click_handler(BUTTON_ID_UP, prv_get_up_click_handler());
prv_configure_click_handler(BUTTON_ID_DOWN, prv_launch_timeline);
prv_configure_click_handler(BUTTON_ID_SELECT, prv_launch_launcher_app);
prv_configure_click_handler(BUTTON_ID_BACK, prv_dismiss_timeline_peek);
}
void watchface_init(void) {
click_manager_init(&s_click_manager);
prv_watchface_configure_click_handlers();
}
void watchface_handle_button_event(PebbleEvent *e) {
if (prv_should_ignore_button_click()) {
return;
}
switch (e->type) {
case PEBBLE_BUTTON_DOWN_EVENT:
click_recognizer_handle_button_down(&s_click_manager.recognizers[e->button.button_id]);
break;
case PEBBLE_BUTTON_UP_EVENT:
click_recognizer_handle_button_up(&s_click_manager.recognizers[e->button.button_id]);
break;
default:
PBL_CROAK("Invalid event type: %u", e->type);
break;
}
}
static void prv_watchface_launch_low_power(void) {
PBL_LOG(LOG_LEVEL_DEBUG, "Switching default watchface to low_power_mode watchface");
app_manager_put_launch_app_event(&(AppLaunchEventConfig) {
.id = APP_ID_LOW_POWER_FACE,
});
}
void watchface_launch_default(const CompositorTransition *animation) {
app_manager_put_launch_app_event(&(AppLaunchEventConfig) {
.id = watchface_get_default_install_id(),
.common.transition = animation,
});
}
static void kernel_callback_watchface_launch(void* data) {
watchface_launch_default(NULL);
}
void command_watch(void) {
launcher_task_add_callback(kernel_callback_watchface_launch, NULL);
}
void watchface_start_low_power(void) {
app_manager_set_minimum_run_level(ProcessAppRunLevelNormal);
prv_watchface_launch_low_power();
}
void watchface_reset_click_manager(void) {
click_manager_reset(&s_click_manager);
}

View File

@@ -0,0 +1,35 @@
/*
* 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.
*/
#pragma once
#include "kernel/events.h"
#include "process_management/app_install_manager.h"
#include "services/common/compositor/compositor.h"
void watchface_init(void);
void watchface_handle_button_event(PebbleEvent *e);
void watchface_set_default_install_id(AppInstallId id);
AppInstallId watchface_get_default_install_id(void);
void watchface_launch_default(const CompositorTransition *animation);
void watchface_start_low_power(void);
void watchface_reset_click_manager(void);

View File

@@ -0,0 +1,89 @@
/*
* 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 "welcome.h"
#include "kernel/event_loop.h"
#include "resource/timeline_resource_ids.auto.h"
#include "services/common/evented_timer.h"
#include "services/common/i18n/i18n.h"
#include "services/common/shared_prf_storage/shared_prf_storage.h"
#include "shell/prefs.h"
#include "system/logging.h"
#include "util/attributes.h"
#include "util/size.h"
static void prv_push_welcome_notification(void *UNUSED data) {
AttributeList notif_attr_list = {};
attribute_list_add_uint32(&notif_attr_list, AttributeIdIconTiny,
TIMELINE_RESOURCE_NOTIFICATION_FLAG);
attribute_list_add_cstring(&notif_attr_list, AttributeIdTitle,
/// Welcome title text welcoming a 3.x user to 4.x
i18n_get("Pebble Updated!", &notif_attr_list));
/// Welcome body text welcoming a 3.x user to 4.x.
const char *welcome_text = i18n_get(
"For activity and sleep tracking, press up from your watch face.\n\n"
"Press down for current and future events.\n\n"
"Read more at blog.pebble.com",
&notif_attr_list);
attribute_list_add_cstring(&notif_attr_list, AttributeIdBody, welcome_text);
attribute_list_add_uint8(&notif_attr_list, AttributeIdBgColor, GColorOrangeARGB8);
AttributeList dismiss_action_attr_list = {};
attribute_list_add_cstring(&dismiss_action_attr_list, AttributeIdTitle,
i18n_get("Dismiss", &notif_attr_list));
int action_id = 0;
TimelineItemAction actions[] = {
{
.id = action_id++,
.type = TimelineItemActionTypeDismiss,
.attr_list = dismiss_action_attr_list,
},
};
TimelineItemActionGroup action_group = {
.num_actions = ARRAY_LENGTH(actions),
.actions = actions,
};
const time_t now = rtc_get_time();
TimelineItem *item = timeline_item_create_with_attributes(
now, 0, TimelineItemTypeNotification, LayoutIdNotification, &notif_attr_list, &action_group);
i18n_free_all(&notif_attr_list);
attribute_list_destroy_list(&notif_attr_list);
attribute_list_destroy_list(&dismiss_action_attr_list);
if (!item) {
PBL_LOG(LOG_LEVEL_WARNING, "Failed to welcome the user.");
return;
}
item->header.from_watch = true;
notifications_add_notification(item);
timeline_item_destroy(item);
welcome_set_welcome_version(WelcomeVersionCurrent);
}
void welcome_push_notification(bool factory_reset_or_first_use) {
const WelcomeVersion version = welcome_get_welcome_version();
// This check only works if it is called before getting started complete is set
if (!factory_reset_or_first_use && (version < WelcomeVersion_4xNormalFirmware)) {
// This has completed getting started on a previous normal firmware, welcome them if the
// version is before 4.x
// We wait some time since notification storage takes time to initialize
launcher_task_add_callback(prv_push_welcome_notification, NULL);
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.
*/
#pragma once
#include <inttypes.h>
#include <stdbool.h>
//! Version of the welcoming of the user to the normal firmware
typedef enum WelcomeVersion {
//! Initial version or never launched normal firmware
WelcomeVersion_InitialVersion = 0,
//! 4.x Normal Firmware
WelcomeVersion_4xNormalFirmware = 1,
WelcomeVersionCount,
//! WelcomeVersion is an increasing version number. WelcomeVersionCurrent must
//! not decrement. This should ensure that the current version is always the latest.
WelcomeVersionCurrent = WelcomeVersionCount - 1,
} WelcomeVersion;
//! Welcomes the user to a newer normal firmware they have not used yet if they have used an older
//! normal firmware and the newer normal firmware warrants a notification.
//! @note This must be called before getting started completed is set in shared prf storage.
void welcome_push_notification(bool factory_reset_or_first_use);
//! Set the welcome version. This is persisted in shell prefs.
void welcome_set_welcome_version(uint8_t version);
//! Get the welcome version
uint8_t welcome_get_welcome_version(void);