pebble/src/fw/popups/alarm_popup.c
2025-01-27 11:38:16 -08:00

280 lines
9.8 KiB
C

/*
* 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 "alarm_popup.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/dialogs/simple_dialog.h"
#include "applib/ui/dialogs/actionable_dialog.h"
#include "applib/ui/vibes.h"
#include "applib/ui/window_stack.h"
#include "kernel/event_loop.h"
#include "kernel/low_power.h"
#include "kernel/pbl_malloc.h"
#include "kernel/ui/modals/modal_manager.h"
#include "resource/resource_ids.auto.h"
#include "services/common/clock.h"
#include "services/common/i18n/i18n.h"
#include "services/common/light.h"
#include "services/common/new_timer/new_timer.h"
#include "services/normal/alarms/alarm.h"
#include "util/time/time.h"
#include <stdio.h>
#include <string.h>
#if !PLATFORM_TINTIN
#include "services/normal/vibes/vibe_client.h"
#include "services/normal/vibes/vibe_score.h"
#endif
#if !TINTIN_FORCE_FIT
#define DIALOG_TIMEOUT_SNOOZE 2000
#define DIALOG_TIMEOUT_DISMISS DIALOG_TIMEOUT_SNOOZE
#define ALARM_PRIORITY (ModalPriorityAlarm)
static WindowStack *prv_get_window_stack(void) {
return modal_manager_get_window_stack(ALARM_PRIORITY);
}
// ----------------------------------------------------------------------------------------------
//! Snooze confirm dialog
static void prv_show_snooze_confirm_dialog(void) {
SimpleDialog *simple_dialog = simple_dialog_create("AlarmSnooze");
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
const char *snooze_text = i18n_noop("Snooze for %d minutes");
char snooze_buf[32];
snprintf(snooze_buf, sizeof(snooze_buf), i18n_get(snooze_text, dialog), alarm_get_snooze_delay());
i18n_free(snooze_text, dialog);
dialog_set_text(dialog, snooze_buf);
dialog_set_icon(dialog, RESOURCE_ID_GENERIC_CONFIRMATION_LARGE);
dialog_set_background_color(dialog, GColorJaegerGreen);
dialog_set_timeout(dialog, DIALOG_TIMEOUT_SNOOZE);
simple_dialog_push(simple_dialog, prv_get_window_stack());
}
// ----------------------------------------------------------------------------------------------
//! Dismiss confirm dialog
static void prv_show_dismiss_confirm_dialog(void) {
SimpleDialog *simple_dialog = simple_dialog_create("AlarmSnooze");
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
const char *dismiss_text = i18n_noop("Alarm dismissed");
dialog_set_text(dialog, i18n_get(dismiss_text, dialog));
i18n_free(dismiss_text, dialog);
dialog_set_icon(dialog, RESOURCE_ID_GENERIC_CONFIRMATION_LARGE);
dialog_set_background_color(dialog, GColorJaegerGreen);
dialog_set_timeout(dialog, DIALOG_TIMEOUT_DISMISS);
simple_dialog_push(simple_dialog, prv_get_window_stack());
}
// ----------------------------------------------------------------------------------------------
//! Main Window
typedef struct {
ActionableDialog *alarm_popup;
GBitmap *bitmap;
GBitmap *action_bar_dismiss;
GBitmap *action_bar_snooze;
ActionBarLayer action_bar;
TimerID vibe_timer;
int max_vibes;
int vibe_count;
#if CAPABILITY_HAS_VIBE_SCORES
VibeScore *vibe_score;
#endif
} AlarmPopupData;
AlarmPopupData *s_alarm_popup_data = NULL;
static void prv_stop_animation_kernel_main_cb(void *callback_context) {
if (s_alarm_popup_data) {
dialog_set_icon((Dialog *) s_alarm_popup_data->alarm_popup,
RESOURCE_ID_ALARM_CLOCK_LARGE_STATIC);
}
}
static void prv_stop_vibes(void) {
if (s_alarm_popup_data->vibe_timer != TIMER_INVALID_ID) {
new_timer_stop(s_alarm_popup_data->vibe_timer);
new_timer_delete(s_alarm_popup_data->vibe_timer);
s_alarm_popup_data->vibe_timer = TIMER_INVALID_ID;
#if CAPABILITY_HAS_VIBE_SCORES
if (s_alarm_popup_data->vibe_score) {
vibe_score_destroy(s_alarm_popup_data->vibe_score);
s_alarm_popup_data->vibe_score = NULL;
}
#endif
}
vibes_cancel();
}
// ----------------------------------------------------------------------------------------------
//! Vibe Timer
#define TINTIN_VIBE_REPEAT_INTERVAL_MS (1000)
#define TINTIN_MAX_VIBES (10 * 60) // 10 minutes at 1 vibe a second
#define TINTIN_LPM_VIBES_PER_MINUTE (10)
#define VIBE_DURATION (10 * SECONDS_PER_MINUTE * MS_PER_SECOND)
static void prv_vibe_kernel_main_cb(void *callback_context) {
if (s_alarm_popup_data) {
if (s_alarm_popup_data->vibe_count < s_alarm_popup_data->max_vibes) {
s_alarm_popup_data->vibe_count++;
#if CAPABILITY_HAS_VIBE_SCORES
vibe_score_do_vibe(s_alarm_popup_data->vibe_score);
#else
if (low_power_is_active()) {
// Only vibe 10 seconds every minute in low_power_mode
_Static_assert(TINTIN_VIBE_REPEAT_INTERVAL_MS == MS_PER_SECOND,
"LPM Vibes timing incorrect");
if (s_alarm_popup_data->vibe_count % SECONDS_PER_MINUTE < TINTIN_LPM_VIBES_PER_MINUTE) {
vibes_long_pulse();
}
} else {
vibes_long_pulse();
}
#endif
}
else {
prv_stop_vibes();
launcher_task_add_callback(prv_stop_animation_kernel_main_cb, NULL);
}
}
}
static void prv_vibe(void *unused) {
launcher_task_add_callback(prv_vibe_kernel_main_cb, NULL);
}
static void prv_start_vibes(void) {
s_alarm_popup_data->vibe_count = 0;
unsigned int vibe_repeat_interval_ms = TINTIN_VIBE_REPEAT_INTERVAL_MS;
#if CAPABILITY_HAS_VIBE_SCORES
if (low_power_is_active()) {
s_alarm_popup_data->vibe_score = vibe_client_get_score(VibeClient_AlarmsLPM);
} else {
s_alarm_popup_data->vibe_score = vibe_client_get_score(VibeClient_Alarms);
}
if (!s_alarm_popup_data->vibe_score) {
return;
}
vibe_repeat_interval_ms = vibe_score_get_duration_ms(s_alarm_popup_data->vibe_score) +
vibe_score_get_repeat_delay_ms(s_alarm_popup_data->vibe_score);
s_alarm_popup_data->max_vibes = DIVIDE_CEIL(VIBE_DURATION, vibe_repeat_interval_ms);
#else
s_alarm_popup_data->max_vibes = TINTIN_MAX_VIBES;
#endif
s_alarm_popup_data->vibe_timer = new_timer_create();
prv_vibe(NULL);
new_timer_start(s_alarm_popup_data->vibe_timer, vibe_repeat_interval_ms, prv_vibe,
NULL, TIMER_START_FLAG_REPEATING);
}
// ----------------------------------------------------------------------------------------------
//! Click Handler
static void prv_dismiss_click_handler(ClickRecognizerRef recognizer, void *data) {
alarm_dismiss_alarm();
prv_show_dismiss_confirm_dialog();
actionable_dialog_pop(s_alarm_popup_data->alarm_popup);
}
static void prv_snooze_click_handler(ClickRecognizerRef recognizer, void *data) {
alarm_set_snooze_alarm();
prv_show_snooze_confirm_dialog();
actionable_dialog_pop(s_alarm_popup_data->alarm_popup);
}
static void prv_click_provider(void *context) {
window_single_click_subscribe(BUTTON_ID_DOWN, prv_dismiss_click_handler);
window_single_click_subscribe(BUTTON_ID_UP, prv_snooze_click_handler);
window_single_click_subscribe(BUTTON_ID_BACK, prv_snooze_click_handler);
}
// ----------------------------------------------------------------------------------------------
//! Main Window Setup
static void prv_setup_action_bar(void) {
ActionBarLayer *action_bar = &s_alarm_popup_data->action_bar;
action_bar_layer_init(action_bar);
action_bar_layer_set_background_color(action_bar, GColorBlack);
s_alarm_popup_data->action_bar_snooze =
gbitmap_create_with_resource(RESOURCE_ID_ACTION_BAR_ICON_SNOOZE);
s_alarm_popup_data->action_bar_dismiss =
gbitmap_create_with_resource(RESOURCE_ID_ACTION_BAR_ICON_X);
action_bar_layer_set_icon(action_bar, BUTTON_ID_UP, s_alarm_popup_data->action_bar_snooze);
action_bar_layer_set_icon(action_bar, BUTTON_ID_DOWN, s_alarm_popup_data->action_bar_dismiss);
action_bar_layer_set_click_config_provider(action_bar, prv_click_provider);
}
static void prv_cleanup_alarm_popup(void *callback_context) {
if (s_alarm_popup_data) {
prv_stop_vibes();
gbitmap_destroy(s_alarm_popup_data->bitmap);
gbitmap_destroy(s_alarm_popup_data->action_bar_snooze);
gbitmap_destroy(s_alarm_popup_data->action_bar_dismiss);
task_free(s_alarm_popup_data);
s_alarm_popup_data = NULL;
}
}
// ----------------------------------------------------------------------------------------------
//! API
#endif
void alarm_popup_push_window(PebbleAlarmClockEvent *event) {
#if !TINTIN_FORCE_FIT
if (s_alarm_popup_data) {
// The window is already visible, don't show another one
return;
}
s_alarm_popup_data = task_malloc_check(sizeof(AlarmPopupData));
*s_alarm_popup_data = (AlarmPopupData){};
s_alarm_popup_data->vibe_timer = TIMER_INVALID_ID;
prv_setup_action_bar();
s_alarm_popup_data->alarm_popup = actionable_dialog_create("Alarm Popup");
actionable_dialog_set_action_bar_type(s_alarm_popup_data->alarm_popup, DialogActionBarCustom,
&s_alarm_popup_data->action_bar);
Dialog *dialog = actionable_dialog_get_dialog(s_alarm_popup_data->alarm_popup);
char display_time[16];
struct tm alarm_tm;
localtime_r(&event->alarm_time, &alarm_tm);
if (clock_is_24h_style()) {
strftime(display_time, 16, "%H:%M", &alarm_tm);
} else {
strftime(display_time, 16, "%I:%M %p", &alarm_tm);
}
dialog_set_text(dialog, display_time);
dialog_set_icon(dialog, RESOURCE_ID_ALARM_CLOCK_LARGE);
dialog_set_background_color(dialog, GColorJaegerGreen);
DialogCallbacks callback = {
.unload = prv_cleanup_alarm_popup,
};
dialog_set_callbacks(dialog, &callback, NULL);
actionable_dialog_push(s_alarm_popup_data->alarm_popup, prv_get_window_stack());
prv_start_vibes();
light_enable_interaction();
#else
return;
#endif
}