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

280
src/fw/popups/alarm_popup.c Normal file
View file

@ -0,0 +1,280 @@
/*
* 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
}

View file

@ -0,0 +1,21 @@
/*
* 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"
void alarm_popup_push_window(PebbleAlarmClockEvent* e);

View file

@ -0,0 +1,74 @@
/*
* 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 "ble_hrm_reminder_popup.h"
#include "drivers/rtc.h"
#include "services/common/i18n/i18n.h"
#include "services/normal/notifications/notifications.h"
#include "services/normal/timeline/timeline.h"
#include "services/normal/timeline/timeline_resources.h"
#include <util/size.h>
void ble_hrm_push_reminder_popup(void) {
AttributeList attr_list = {};
const char *body = i18n_get("Your heart rate has been shared with an app on your phone for "
"several hours. This could affect your battery. Stop sharing now?",
&attr_list);
attribute_list_add_cstring(&attr_list, AttributeIdBody, body);
attribute_list_add_uint32(&attr_list, AttributeIdIconTiny,
TIMELINE_RESOURCE_BLE_HRM_SHARING);
attribute_list_add_uint8(&attr_list, AttributeIdBgColor, GColorOrangeARGB8);
AttributeList dismiss_action_attr_list = {};
attribute_list_add_cstring(&dismiss_action_attr_list, AttributeIdTitle,
i18n_get("Dismiss", &attr_list));
AttributeList stop_action_attr_list = {};
attribute_list_add_cstring(&stop_action_attr_list, AttributeIdTitle,
i18n_get("Stop Sharing Heart Rate", &attr_list));
TimelineItemActionGroup action_group = {
.num_actions = 2,
.actions = (TimelineItemAction[]) {
{
.id = 0,
.type = TimelineItemActionTypeDismiss,
.attr_list = dismiss_action_attr_list,
},
{
.id = 1,
.type = TimelineItemActionTypeBLEHRMStopSharing,
.attr_list = stop_action_attr_list,
},
},
};
TimelineItem *item = timeline_item_create_with_attributes(rtc_get_time(), 0,
TimelineItemTypeNotification,
LayoutIdNotification, &attr_list,
&action_group);
i18n_free_all(&attr_list);
attribute_list_destroy_list(&attr_list);
attribute_list_destroy_list(&dismiss_action_attr_list);
attribute_list_destroy_list(&stop_action_attr_list);
notifications_add_notification(item);
timeline_item_destroy(item);
}

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 ble_hrm_push_reminder_popup(void);

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 "services/normal/bluetooth/ble_hrm.h"
#include "applib/graphics/gcolor_definitions.h"
#include "applib/ui/dialogs/actionable_dialog.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/dialogs/simple_dialog.h"
#include "kernel/ui/modals/modal_manager.h"
#include "applib/ui/vibes.h"
#include "resource/resource_ids.auto.h"
#include "services/common/i18n/i18n.h"
#include <util/size.h>
#include <stdio.h>
#define BLE_HRM_CONFIRMATION_TIMEOUT_MS (2000)
static void prv_respond(bool is_granted, ActionableDialog *actionable_dialog) {
BLEHRMSharingRequest *sharing_request = actionable_dialog->dialog.callback_context;
ble_hrm_handle_sharing_request_response(is_granted, sharing_request);
actionable_dialog_pop(actionable_dialog);
if (is_granted) {
SimpleDialog *simple_dialog = simple_dialog_create("Sharing");
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
const char *msg = i18n_get("Sharing Heart Rate", dialog);
dialog_set_text(dialog, msg);
dialog_set_icon(dialog, RESOURCE_ID_BLE_HRM_SHARED);
dialog_set_timeout(dialog, BLE_HRM_CONFIRMATION_TIMEOUT_MS);
simple_dialog_set_icon_animated(simple_dialog, false);
i18n_free(msg, dialog);
simple_dialog_push(simple_dialog, modal_manager_get_window_stack(ModalPriorityGeneric));
}
}
static void prv_confirm_cb(ClickRecognizerRef recognizer, void *context) {
prv_respond(true /* is_granted */, (ActionableDialog *)context);
}
static void prv_back_cb(ClickRecognizerRef recognizer, void *context) {
prv_respond(false /* is_granted */, (ActionableDialog *)context);
}
static void prv_shutdown_click_provider(void *context) {
window_single_click_subscribe(BUTTON_ID_UP, prv_confirm_cb);
window_single_click_subscribe(BUTTON_ID_BACK, prv_back_cb);
window_single_click_subscribe(BUTTON_ID_DOWN, prv_back_cb);
}
void ble_hrm_push_sharing_request_window(BLEHRMSharingRequest *sharing_request) {
ActionableDialog *a_dialog = actionable_dialog_create("HRM Sharing");
Dialog *dialog = actionable_dialog_get_dialog(a_dialog);
dialog->callback_context = sharing_request;
actionable_dialog_set_action_bar_type(a_dialog, DialogActionBarConfirmDecline, NULL);
actionable_dialog_set_click_config_provider(a_dialog, prv_shutdown_click_provider);
dialog_set_text_color(dialog, GColorWhite);
dialog_set_background_color(dialog, GColorCobaltBlue);
dialog_set_icon(dialog, RESOURCE_ID_BLE_HRM_SHARE_REQUEST_LARGE);
dialog_set_text(dialog, i18n_get("Share heart rate?", a_dialog));
i18n_free_all(a_dialog);
actionable_dialog_push(a_dialog, modal_manager_get_window_stack(ModalPriorityGeneric));
const uint32_t const heart_beat_durations[] = { 100, 100, 150, 600, 100, 100, 150 };
VibePattern heart_beat_pattern = {
.durations = heart_beat_durations,
.num_segments = ARRAY_LENGTH(heart_beat_durations),
};
vibes_enqueue_custom_pattern(heart_beat_pattern);
}

View file

@ -0,0 +1,26 @@
/*
* 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
#if CAPABILITY_HAS_BUILTIN_HRM
typedef struct BLEHRMSharingRequest BLEHRMSharingRequest;
//! @note Must be called from KernelMain
void ble_hrm_push_sharing_request_window(BLEHRMSharingRequest *sharing_request);
#endif

View file

@ -0,0 +1,37 @@
/*
* 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 "ble_hrm_stop_sharing_popup.h"
#include "applib/ui/dialogs/simple_dialog.h"
#include "resource/resource_ids.auto.h"
#include "services/common/i18n/i18n.h"
#include "util/time/time.h"
#define BLE_HRM_CONFIRMATION_TIMEOUT_MS (2 * MS_PER_SECOND)
SimpleDialog *ble_hrm_stop_sharing_popup_create(void) {
SimpleDialog *simple_dialog = simple_dialog_create("Stopped Sharing");
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
const char *msg = i18n_get("Heart Rate Not Shared", dialog);
dialog_set_text(dialog, msg);
dialog_set_icon(dialog, RESOURCE_ID_BLE_HRM_NOT_SHARED);
dialog_set_timeout(dialog, BLE_HRM_CONFIRMATION_TIMEOUT_MS);
simple_dialog_set_icon_animated(simple_dialog, false);
i18n_free(msg, dialog);
return simple_dialog;
}

View file

@ -0,0 +1,21 @@
/*
* 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
typedef struct SimpleDialog SimpleDialog;
SimpleDialog *ble_hrm_stop_sharing_popup_create(void);

View file

@ -0,0 +1,695 @@
/*
* 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.
*/
#define FILE_LOG_COLOR LOG_COLOR_BLUE
#include "bluetooth_pairing_ui.h"
#include "applib/fonts/fonts.h"
#include "applib/ui/kino/kino_layer.h"
#include "applib/ui/kino/kino_reel.h"
#include "applib/ui/ui.h"
#include "applib/ui/window_private.h"
#include "applib/ui/window_stack.h"
#include "comm/bt_lock.h"
#include "kernel/events.h"
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "kernel/ui/modals/modal_manager.h"
#include "kernel/ui/system_icons.h"
#include "resource/resource_ids.auto.h"
#include "services/common/i18n/i18n.h"
#include "services/common/light.h"
#include "services/common/new_timer/new_timer.h"
#include "services/common/system_task.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/size.h"
#include <bluetooth/id.h>
#include <bluetooth/pairing_confirm.h>
#include <stdio.h>
#include <string.h>
#define CODE_BUF_SIZE 16
#define MAX_PAIR_STR_LEN 16
typedef enum {
BTPairingUIStateAwaitingUserConfirmation, // It's possible to go from here straight to Failed
BTPairingUIStateAwaitingResult,
BTPairingUIStateSuccess,
BTPairingUIStateFailed,
} BTPairingUIState;
typedef struct BTPairingUIData {
Window window;
KinoLayer kino_layer;
KinoReel *reel;
GBitmap *approve_bitmap;
GBitmap *decline_bitmap;
ActionBarLayer action_bar_layer;
Layer info_text_mask_layer;
PropertyAnimation *info_text_out_animation;
PropertyAnimation *info_text_in_animation;
// The info text layers store show the text that prompts the user to pair
TextLayer info_text_layer;
char info_text_layer_buffer[MAX_PAIR_STR_LEN];
#ifdef RECOVERY_FW
GRect pair_text_area;
GRect above_pair_text_area;
TextLayer info_text_layer2;
char info_text_layer2_buffer[MAX_PAIR_STR_LEN];
int num_strings_shown;
int translated_str_idx;
#endif
TextLayer device_name_text_layer;
char device_name_layer_buffer[BT_DEVICE_NAME_BUFFER_SIZE];
TextLayer code_text_layer;
char code_text_layer_buffer[CODE_BUF_SIZE];
TimerID timer;
BTPairingUIState ui_state;
const PairingUserConfirmationCtx *ctx;
} BTPairingUIData;
//! This pointer and the data it points to should only be accessed from KernelMain
static BTPairingUIData *s_data_ptr = NULL;
static void prv_handle_pairing_complete(bool success);
#ifdef RECOVERY_FW // PRF -- animate through a few hard-coded text strings for "Pair?"
static void prv_animate_info_text(BTPairingUIData *data);
static void prv_info_text_animation_stopped(Animation *anim, bool finished, void *context) {
if (!s_data_ptr) {
return;
}
// Reset the text box positions
layer_set_frame(&s_data_ptr->info_text_layer.layer, &s_data_ptr->pair_text_area);
layer_set_frame(&s_data_ptr->info_text_layer2.layer, &s_data_ptr->above_pair_text_area);
if (s_data_ptr->ui_state == BTPairingUIStateAwaitingUserConfirmation) {
// Reschedule animations
prv_animate_info_text(s_data_ptr);
}
}
static void prv_cleanup_prf_animations(BTPairingUIData *data) {
animation_unschedule(property_animation_get_animation(data->info_text_in_animation));
animation_unschedule(property_animation_get_animation(data->info_text_out_animation));
layer_set_hidden(&data->info_text_layer2.layer, true);
}
typedef struct {
const char *string;
const char *font_key;
} Translation;
static void prv_update_text_layer_with_translation(TextLayer *text_layer,
const Translation *translation) {
text_layer_set_text(text_layer, translation->string);
text_layer_set_font(text_layer, fonts_get_system_font(translation->font_key));
}
static void prv_update_prf_info_text_layers_text(BTPairingUIData *data) {
#if PLATFORM_ROBERT || PLATFORM_CALCULUS
const char *font_key_default = FONT_KEY_GOTHIC_28_BOLD;
const char *font_key_japanese = FONT_KEY_MINCHO_24_PAIR;
#else
const char *font_key_default = FONT_KEY_GOTHIC_24_BOLD;
const char *font_key_japanese = FONT_KEY_MINCHO_20_PAIR;
#endif
const Translation english_translation = { "Pair?", font_key_default };
const Translation translations[] = {
{ "Koppeln?", font_key_default }, // German
{ "Jumeler?", font_key_default }, // French
{ "¿Enlazar?", font_key_default }, // Spanish
{ "Associare?", font_key_default }, // Italian
{ "Emparelhar?", font_key_default }, // Portuguese
{ "ペアリング", font_key_japanese }, // Japanese
{ "配对", font_key_default }, // Chinese (traditional?)
{ "配對", font_key_default } // Chinese (simplified?)
};
// The strings should be displayed in the following pattern:
// english, translated, translated, english, translated, translated, ...
if (data->num_strings_shown % 3 == 0) {
prv_update_text_layer_with_translation(&data->info_text_layer, &english_translation);
} else {
prv_update_text_layer_with_translation(&data->info_text_layer,
&translations[data->translated_str_idx]);
data->translated_str_idx = (data->translated_str_idx + 1) % ARRAY_LENGTH(translations);
}
if ((data->num_strings_shown + 1) % 3 == 0) {
prv_update_text_layer_with_translation(&data->info_text_layer2, &english_translation);
} else {
prv_update_text_layer_with_translation(&data->info_text_layer2,
&translations[data->translated_str_idx]);
}
data->num_strings_shown++;
}
static void prv_animate_info_text(BTPairingUIData *data) {
prv_update_prf_info_text_layers_text(data);
animation_schedule(property_animation_get_animation(data->info_text_in_animation));
animation_schedule(property_animation_get_animation(data->info_text_out_animation));
}
static void prv_add_prf_layers(GRect pair_text_area, BTPairingUIData *data) {
GRect below_pair_text = GRect(0, 38, pair_text_area.size.w, 30);
GRect above_pair_text = GRect(0, -34, pair_text_area.size.w, 30);
data->pair_text_area = pair_text_area;
data->above_pair_text_area = above_pair_text;
TextLayer *info_text_layer2 = &data->info_text_layer2;
text_layer_init_with_parameters(info_text_layer2,
&above_pair_text,
data->info_text_layer2_buffer,
fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD),
GColorBlack, GColorClear, GTextAlignmentCenter,
GTextOverflowModeTrailingEllipsis);
layer_add_child(&data->info_text_mask_layer, &info_text_layer2->layer);
// The order in which the languages are shown (see prv_update_prf_info_text_layers_text()) means
// that in order to see each translated language twice we must show 15 total strings.
// The Bluetooth SPP popup will timeout in 30 seconds so each animation + delay = 30/15 = 2s
const int animation_duration_ms = 300;
const int animation_delay_ms = 1700;
// This is the text that is currently not visible and animates into view
data->info_text_in_animation =
property_animation_create_layer_frame(&data->info_text_layer2.layer,
&above_pair_text, &pair_text_area);
PBL_ASSERTN(data->info_text_in_animation);
Animation *animation = property_animation_get_animation(data->info_text_in_animation);
animation_set_auto_destroy(animation, false);
animation_set_duration(animation, animation_duration_ms);
animation_set_delay(animation, animation_delay_ms);
// This is the text that is currently visible and animates out of view
data->info_text_out_animation =
property_animation_create_layer_frame(&data->info_text_layer.layer,
&pair_text_area, &below_pair_text);
PBL_ASSERTN(data->info_text_out_animation);
animation = property_animation_get_animation(data->info_text_out_animation);
animation_set_auto_destroy(animation, false);
animation_set_duration(animation, animation_duration_ms);
animation_set_delay(animation, animation_delay_ms);
// We only need a stop handler for one of the animations as they should finish at the same time
AnimationHandlers handlers = {
.stopped = prv_info_text_animation_stopped,
};
animation_set_handlers(animation, handlers, NULL);
}
static void prv_initialize_info_text(BTPairingUIData *data) {
prv_animate_info_text(data);
}
static void prv_deinitialize_info_text(BTPairingUIData *data) {
}
#else // Normal FW -- use i18n text for "Pair?"
static void prv_cleanup_prf_animations(BTPairingUIData *data) {
}
static void prv_add_prf_layers(GRect pair_text_area, BTPairingUIData *data) {
}
static void prv_initialize_info_text(BTPairingUIData *data) {
strncpy(data->info_text_layer_buffer, (char *)i18n_get("Pair?", data), MAX_PAIR_STR_LEN);
}
static void prv_deinitialize_info_text(BTPairingUIData *data) {
i18n_free_all(data);
}
#endif
static uint32_t prv_resource_id_for_state(BTPairingUIState state) {
switch (state) {
case BTPairingUIStateAwaitingUserConfirmation:
return RESOURCE_ID_BT_PAIR_CONFIRMATION;
case BTPairingUIStateAwaitingResult:
return RESOURCE_ID_BT_PAIR_APPROVE_ON_PHONE;
case BTPairingUIStateSuccess:
return RESOURCE_ID_BT_PAIR_SUCCESS;
case BTPairingUIStateFailed:
return RESOURCE_ID_BT_PAIR_FAILURE;
default:
WTF;
}
}
static void prv_adjust_background_frame_for_state(BTPairingUIData *data) {
GAlign alignment;
const int16_t width_of_sidebar = data->action_bar_layer.layer.frame.size.w;
const int16_t window_width = data->window.layer.bounds.size.w;
const int16_t config_width = window_width - width_of_sidebar + 10;
int16_t x_offset, y_offset, width;
switch (data->ui_state) {
case BTPairingUIStateAwaitingUserConfirmation:
alignment = GAlignTopLeft;
#if PLATFORM_ROBERT || PLATFORM_CALCULUS
x_offset = 39;
y_offset = 85;
#else
x_offset = PBL_IF_RECT_ELSE(10, 31);
y_offset = PBL_IF_RECT_ELSE(44, 46);
#endif
width = config_width;
break;
case BTPairingUIStateAwaitingResult:
alignment = GAlignLeft;
#if PLATFORM_ROBERT || PLATFORM_CALCULUS
x_offset = 76;
y_offset = 30;
#else
x_offset = PBL_IF_RECT_ELSE(49, 67);
y_offset = PBL_IF_RECT_ELSE(22, 25);
#endif
width = window_width;
break;
case BTPairingUIStateFailed:
case BTPairingUIStateSuccess:
alignment = GAlignTop;
#if PLATFORM_ROBERT || PLATFORM_CALCULUS
x_offset = 0;
y_offset = 59;
#else
x_offset = 2;
y_offset = PBL_IF_RECT_ELSE(30, 36);
#endif
width = window_width;
break;
default:
WTF;
}
GRect kino_area;
kino_area = GRect(x_offset, y_offset, width, data->window.layer.bounds.size.h);
kino_layer_set_alignment(&data->kino_layer, alignment);
layer_set_frame(&data->kino_layer.layer, &kino_area);
kino_layer_set_reel_with_resource(&data->kino_layer, prv_resource_id_for_state(data->ui_state));
}
static void prv_send_response(bool is_confirmed) {
if (!s_data_ptr) {
return;
}
bt_lock();
bt_driver_pairing_confirm(s_data_ptr->ctx, is_confirmed);
bt_unlock();
}
static bool prv_has_device_name(BTPairingUIData *data) {
return (strlen(data->device_name_layer_buffer) != 0);
}
static bool prv_has_confirmation_token(BTPairingUIData *data) {
return (strlen(data->code_text_layer_buffer) != 0);
}
static void prv_exit_awaiting_user_confirmation(BTPairingUIData *data) {
// Remove UI components that are not needed any more after the user input confirmation screen:
prv_cleanup_prf_animations(data);
layer_set_hidden(&data->info_text_layer.layer, true);
if (prv_has_device_name(data)) {
layer_remove_from_parent(&data->device_name_text_layer.layer);
}
if (prv_has_confirmation_token(data)) {
layer_remove_from_parent(&data->code_text_layer.layer);
}
// Disable all buttons in this screen:
action_bar_layer_remove_from_window(&data->action_bar_layer);
action_bar_layer_set_click_config_provider(&data->action_bar_layer, NULL);
}
static void prv_confirm_click_handler(ClickRecognizerRef recognizer, void *ctx) {
Window *window = (Window *) ctx;
BTPairingUIData *data = window_get_user_data(window);
PBL_ASSERTN(data->ui_state == BTPairingUIStateAwaitingUserConfirmation);
prv_exit_awaiting_user_confirmation(data);
data->ui_state = BTPairingUIStateAwaitingResult;
prv_send_response(true /* is_confirmed */);
prv_adjust_background_frame_for_state(data);
}
static void prv_decline_click_handler(ClickRecognizerRef recognizer, void *ctx) {
Window *window = (Window *) ctx;
BTPairingUIData *data = window_get_user_data(window);
PBL_ASSERTN(data->ui_state == BTPairingUIStateAwaitingUserConfirmation);
prv_send_response(false /* is_confirmed */);
// Not updating ui_state, the handler is capable of dealing with transitioning from
// BTPairingUIStateAwaitingUserConfirmation directly to BTPairingUIStateFailed
prv_handle_pairing_complete(false /* success */);
}
static void prv_user_confirmation_click_config_provider(void *unused) {
window_single_click_subscribe(BUTTON_ID_UP, prv_confirm_click_handler);
window_single_click_subscribe(BUTTON_ID_DOWN, prv_decline_click_handler);
}
static void prv_window_load(Window *window) {
BTPairingUIData *data = window_get_user_data(window);
window_set_background_color(&data->window, PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite));
const int32_t width_of_action_bar_with_padding = ACTION_BAR_WIDTH + PBL_IF_RECT_ELSE(2, -4);
const int32_t width = window->layer.bounds.size.w - width_of_action_bar_with_padding;
const int32_t x_offset = PBL_IF_RECT_ELSE(0, 22);
#if PLATFORM_ROBERT || PLATFORM_CALCULUS
const int32_t info_text_y_offset = 36;
#else
const int32_t info_text_y_offset = PBL_IF_RECT_ELSE(10, 12);
#endif
KinoLayer *kino_layer = &data->kino_layer;
kino_layer_init(kino_layer, &window->layer.bounds);
layer_add_child(&window->layer, &kino_layer->layer);
#if PLATFORM_ROBERT || PLATFORM_CALCULUS
GRect pair_text_area = GRect(0, -2, width, 44);
#else
GRect pair_text_area = GRect(0, -2, width, 30);
#endif
#if PLATFORM_ROBERT || PLATFORM_CALCULUS
layer_set_frame(&data->info_text_mask_layer, &GRect(x_offset, info_text_y_offset, width, 30));
#else
layer_set_frame(&data->info_text_mask_layer, &GRect(x_offset, info_text_y_offset, width, 26));
#endif
layer_set_clips(&data->info_text_mask_layer, true);
layer_add_child(&window->layer, &data->info_text_mask_layer);
TextLayer *info_text_layer = &data->info_text_layer;
text_layer_init_with_parameters(info_text_layer,
&pair_text_area,
data->info_text_layer_buffer,
#if PLATFORM_ROBERT || PLATFORM_CALCULUS
fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD),
#else
fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD),
#endif
GColorBlack, GColorClear, GTextAlignmentCenter,
GTextOverflowModeTrailingEllipsis);
layer_add_child(&data->info_text_mask_layer, &info_text_layer->layer);
ActionBarLayer *action_bar_layer = &data->action_bar_layer;
action_bar_layer_init(action_bar_layer);
action_bar_layer_add_to_window(action_bar_layer, window);
data->approve_bitmap = gbitmap_create_with_resource(RESOURCE_ID_ACTION_BAR_ICON_CHECK);
data->decline_bitmap = gbitmap_create_with_resource(RESOURCE_ID_ACTION_BAR_ICON_X);
action_bar_layer_set_click_config_provider(action_bar_layer,
prv_user_confirmation_click_config_provider);
action_bar_layer_set_icon(action_bar_layer, BUTTON_ID_UP, data->approve_bitmap);
action_bar_layer_set_icon(action_bar_layer, BUTTON_ID_DOWN, data->decline_bitmap);
action_bar_layer_set_context(action_bar_layer, data);
prv_add_prf_layers(pair_text_area, data);
const int16_t y_offset = PBL_IF_RECT_ELSE(0, 2);
// Device name:
if (prv_has_device_name(data)) {
TextLayer *device_name_layer = &data->device_name_text_layer;
text_layer_init_with_parameters(device_name_layer,
&GRect(x_offset, 122 + y_offset, width - x_offset, 30),
data->device_name_layer_buffer,
fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD),
GColorBlack, GColorClear, GTextAlignmentCenter,
GTextOverflowModeTrailingEllipsis);
layer_add_child(&window->layer, &device_name_layer->layer);
}
// Confirmation token:
if (prv_has_confirmation_token(data)) {
TextLayer *code_text_layer = &data->code_text_layer;
text_layer_init_with_parameters(code_text_layer,
&GRect(x_offset, 148 + y_offset, width, 30),
data->code_text_layer_buffer,
fonts_get_system_font(FONT_KEY_GOTHIC_14),
GColorBlack, GColorClear, GTextAlignmentCenter,
GTextOverflowModeTrailingEllipsis);
layer_add_child(&window->layer, &code_text_layer->layer);
}
prv_adjust_background_frame_for_state(data);
prv_initialize_info_text(data);
}
static void prv_window_unload(Window *window) {
BTPairingUIData *data = window_get_user_data(window);
if (data) {
kino_layer_deinit(&data->kino_layer);
text_layer_deinit(&data->info_text_layer);
text_layer_deinit(&data->device_name_text_layer);
text_layer_deinit(&data->code_text_layer);
gbitmap_destroy(data->approve_bitmap);
gbitmap_destroy(data->decline_bitmap);
action_bar_layer_deinit(&data->action_bar_layer);
new_timer_delete(data->timer);
if (data->ui_state == BTPairingUIStateAwaitingUserConfirmation) {
prv_send_response(false /* is_confirmed */);
}
prv_deinitialize_info_text(data);
property_animation_destroy(data->info_text_in_animation);
property_animation_destroy(data->info_text_out_animation);
kernel_free(data);
}
s_data_ptr = NULL;
}
static void prv_show_failure_kernel_main_cb(void *unused) {
prv_handle_pairing_complete(false /* success */);
}
static void prv_pairing_timeout_timer_callback(void *unused) {
PBL_LOG(LOG_LEVEL_WARNING, "SSP timeout fired!");
launcher_task_add_callback(prv_show_failure_kernel_main_cb, NULL);
}
static void prv_pop_window(void) {
if (s_data_ptr) {
window_stack_remove(&s_data_ptr->window, true /* animated */);
}
}
static void prv_pop_window_kernel_main_cb(void* unused) {
prv_pop_window();
}
static void prv_pop_window_timer_callback(void *unused) {
launcher_task_add_callback(prv_pop_window_kernel_main_cb, NULL);
}
static void prv_push_pairing_window(void) {
BTPairingUIData *data = s_data_ptr;
Window *window = &data->window;
window_init(window, WINDOW_NAME("Bluetooth SSP"));
window_set_window_handlers(window, &(WindowHandlers) {
.load = prv_window_load,
.unload = prv_window_unload,
});
window_set_user_data(window, data);
window_set_overrides_back_button(window, true);
modal_window_push(window, ModalPriorityCritical, true /* animated */);
vibes_double_pulse();
light_enable_interaction();
// This timeout is 0.5s longer than the BT Spec's timeout, to decrease the chances of getting
// a success confirmation right at the max allowed time of 30 secs:
const uint32_t timeout_ms = (30 * 1000) + 500;
data->timer = new_timer_create();
bool success = new_timer_start(data->timer, timeout_ms, prv_pairing_timeout_timer_callback, data,
0 /* flags */);
PBL_ASSERTN(success);
}
static void prv_pop_click_handler(ClickRecognizerRef recognizer, void *ctx) {
prv_pop_window();
}
static void prv_success_or_failure_click_config_provider(void *unused) {
window_single_click_subscribe(BUTTON_ID_BACK, prv_pop_click_handler);
window_single_click_subscribe(BUTTON_ID_UP, prv_pop_click_handler);
window_single_click_subscribe(BUTTON_ID_SELECT, prv_pop_click_handler);
window_single_click_subscribe(BUTTON_ID_DOWN, prv_pop_click_handler);
}
static void prv_create_new_pairing_data(void) {
// If we already have a window up, remove that before we push another
if (s_data_ptr) {
window_stack_remove(&s_data_ptr->window, true /* animated */);
}
BTPairingUIData *data = kernel_malloc_check(sizeof(BTPairingUIData));
*data = (BTPairingUIData){};
data->ui_state = BTPairingUIStateAwaitingUserConfirmation;
s_data_ptr = data;
}
static void prv_handle_confirmation_request(const PairingUserConfirmationCtx *ctx,
PebbleBluetoothPairingConfirmationInfo *info) {
prv_create_new_pairing_data();
s_data_ptr->ctx = ctx;
strncpy(s_data_ptr->device_name_layer_buffer, info->device_name ?: "",
sizeof(s_data_ptr->device_name_layer_buffer));
strncpy(s_data_ptr->code_text_layer_buffer, info->confirmation_token ?: "",
sizeof(s_data_ptr->code_text_layer_buffer));
prv_push_pairing_window();
}
static void prv_handle_pairing_complete(bool success) {
if (!s_data_ptr) {
PBL_LOG(LOG_LEVEL_WARNING, "Dialog was not present, but got complete (%u) event", success);
return;
}
BTPairingUIData *data = s_data_ptr;
if (data->ui_state == BTPairingUIStateAwaitingUserConfirmation) {
prv_exit_awaiting_user_confirmation(data);
} else if (data->ui_state != BTPairingUIStateAwaitingResult) {
PBL_LOG(LOG_LEVEL_WARNING,
"Got completion (%u) but not right state", success);
return;
}
PBL_LOG(LOG_LEVEL_DEBUG, "Got Completion! %u", success);
data->ui_state = success ? BTPairingUIStateSuccess : BTPairingUIStateFailed;
prv_adjust_background_frame_for_state(data);
if (!new_timer_stop(data->timer)) {
// Timer was already executing...
if (success) {
PBL_LOG(LOG_LEVEL_WARNING, "Timeout cb executing while received successful completion event");
}
}
// On failure, leave the message on screen for 60 seconds, on success, only for 5 seconds:
const uint32_t timeout_ms = (success ? 5 : 60) * 1000;
new_timer_start(data->timer, timeout_ms, prv_pop_window_timer_callback, NULL, 0 /* flags */);
window_set_click_config_provider(&data->window, prv_success_or_failure_click_config_provider);
vibes_short_pulse();
light_enable_interaction();
}
void bluetooth_pairing_ui_handle_event(PebbleBluetoothPairEvent *event) {
PBL_ASSERT_TASK(PebbleTask_KernelMain);
switch (event->type) {
case PebbleBluetoothPairEventTypePairingUserConfirmation:
prv_handle_confirmation_request(event->ctx, event->confirmation_info);
break;
case PebbleBluetoothPairEventTypePairingComplete:
if (s_data_ptr && s_data_ptr->ctx == event->ctx) {
prv_handle_pairing_complete(event->success);
} else {
PBL_LOG(LOG_LEVEL_ERROR, "Got complete event for unknown process %p vs %p",
event->ctx, s_data_ptr ? s_data_ptr->ctx : NULL);
}
break;
default:
WTF;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// BT Driver callback implementations:
static void prv_put_pairing_event(const PebbleBluetoothPairEvent *pair_event) {
PebbleEvent event = {
.type = PEBBLE_BT_PAIRING_EVENT,
.bluetooth.pair = *pair_event,
};
event_put(&event);
}
static void prv_copy_string_and_move_cursor(const char *in_str, char **out_str, uint8_t **cursor) {
if (!in_str) {
return;
}
size_t str_size_bytes = strlen(in_str) + 1;
strncpy((char *)*cursor, in_str, str_size_bytes);
*out_str = (char *) *cursor;
*cursor += str_size_bytes;
}
void bt_driver_cb_pairing_confirm_handle_request(const PairingUserConfirmationCtx *ctx,
const char *device_name,
const char *confirmation_token) {
// events.c clean-up (see event_deinit) can only clean up one associated heap allocation,
// so put everything in a single buffer:
size_t device_name_len = device_name ? (strlen(device_name) + 1) : 0;
size_t token_len = confirmation_token ? (strlen(confirmation_token) + 1) : 0;
size_t info_len = (sizeof(PebbleBluetoothPairingConfirmationInfo) + device_name_len + token_len);
uint8_t *cursor = (uint8_t *)kernel_zalloc_check(info_len);
PebbleBluetoothPairingConfirmationInfo *confirmation_info =
(PebbleBluetoothPairingConfirmationInfo *)cursor;
cursor += sizeof(PebbleBluetoothPairingConfirmationInfo);
char *device_name_copy = NULL;
prv_copy_string_and_move_cursor(device_name, &device_name_copy, &cursor);
char *confirmation_token_copy = NULL;
prv_copy_string_and_move_cursor(confirmation_token, &confirmation_token_copy, &cursor);
*confirmation_info = (PebbleBluetoothPairingConfirmationInfo) {
.device_name = device_name_copy,
.confirmation_token = confirmation_token_copy,
};
PebbleBluetoothPairEvent pair_event = {
.type = PebbleBluetoothPairEventTypePairingUserConfirmation,
.ctx = ctx,
.confirmation_info = confirmation_info,
};
prv_put_pairing_event(&pair_event);
}
void bt_driver_cb_pairing_confirm_handle_completed(const PairingUserConfirmationCtx *ctx,
bool success) {
PebbleBluetoothPairEvent pair_event = {
.type = PebbleBluetoothPairEventTypePairingComplete,
.ctx = ctx,
.success = success,
};
prv_put_pairing_event(&pair_event);
}

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
#include "kernel/events.h"
void bluetooth_pairing_ui_handle_event(PebbleBluetoothPairEvent *event);

199
src/fw/popups/crashed_ui.c Normal file
View file

@ -0,0 +1,199 @@
/*
* 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 "crashed_ui.h"
#include "services/common/light.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/dialogs/actionable_dialog.h"
#include "applib/ui/dialogs/expandable_dialog.h"
#include "applib/ui/window_stack.h"
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "kernel/ui/modals/modal_manager.h"
#include "process_management/app_install_manager.h"
#include "process_management/app_manager.h"
#include "process_management/worker_manager.h"
#include "resource/resource_ids.auto.h"
#include "services/common/i18n/i18n.h"
#include <stdio.h>
typedef struct {
ActionableDialog *actionable_dialog;
ActionBarLayer action_bar;
GBitmap check_icon;
GBitmap x_icon;
AppInstallId app_install_id;
} WorkerCrashDialogData;
static void prv_worker_crash_dialog_unload(void *context) {
WorkerCrashDialogData *data = context;
action_bar_layer_deinit(&data->action_bar);
gbitmap_deinit(&data->check_icon);
gbitmap_deinit(&data->x_icon);
kernel_free(data);
}
static WindowStack *prv_get_window_stack(void) {
return modal_manager_get_window_stack(ModalPriorityAlert);
}
static void prv_worker_crash_button_up_handler(ClickRecognizerRef recognizer, void *context) {
WorkerCrashDialogData *data = context;
// Push an event to launch the app for the worker that crashed
app_manager_put_launch_app_event(&(AppLaunchEventConfig) {
.id = data->app_install_id,
});
// Pop the worker crash dialog
actionable_dialog_pop(data->actionable_dialog);
}
static void prv_worker_crash_button_down_handler(ClickRecognizerRef recognizer, void *context) {
WorkerCrashDialogData *data = context;
// Have the worker manager launch the next worker
worker_manager_launch_next_worker(data->app_install_id);
// Pop the worker crash dialog
actionable_dialog_pop(data->actionable_dialog);
}
static void prv_worker_crash_click_config_provider(void *context) {
window_single_click_subscribe(BUTTON_ID_UP, prv_worker_crash_button_up_handler);
window_single_click_subscribe(BUTTON_ID_DOWN, prv_worker_crash_button_down_handler);
}
//! Configure a crash dialog with the given crash reason text
//! @param dialog The dialog to configure
//! @param crash_reason The crash reason text
static void prv_configure_crash_dialog(Dialog *dialog, const char *crash_reason) {
dialog_set_text(dialog, i18n_get(crash_reason, dialog));
i18n_free(crash_reason, dialog);
dialog_set_icon(dialog, RESOURCE_ID_GENERIC_WARNING_TINY);
dialog_set_vibe(dialog, true);
}
//! Construct a worker crash reason string
//! @param app_install_id The worker's install id
//! @return The resulting string, which must be freed by the caller
static char *prv_create_worker_crash_reason_string(AppInstallId app_install_id) {
// Get the information about the app whose worker crashed
AppInstallEntry entry;
const bool app_found = app_install_get_entry_for_install_id(app_install_id, &entry);
// Construct the crash reason string (copied by the dialog, so we don't have to free it ourselves)
// "Worker " is 7, optional space is 1, up to 15 for app name, rest of string is 32, 1 for '\0'
const uint8_t MAX_APP_NAME_STRING_LENGTH = 15;
const uint8_t CRASH_REASON_BUFFER_SIZE = 7 + 1 + MAX_APP_NAME_STRING_LENGTH + 32 + 1;
char *crash_reason = kernel_zalloc_check(CRASH_REASON_BUFFER_SIZE);
const char *crash_str = i18n_noop("%s%.*s is not responding.\n\nOpen app?");
sniprintf(crash_reason, CRASH_REASON_BUFFER_SIZE,
i18n_get(crash_str, crash_reason),
app_found ? " " : "",
MAX_APP_NAME_STRING_LENGTH,
app_found ? entry.name : "");
i18n_free(crash_str, crash_reason);
return crash_reason;
}
static void prv_push_worker_crash_dialog(void *context) {
const AppInstallId app_install_id = (AppInstallId) context;
WorkerCrashDialogData *data = kernel_zalloc_check(sizeof(WorkerCrashDialogData));
data->app_install_id = app_install_id;
// Initialize icons for the worker crash dialog's action bar
GBitmap *check_icon = &data->check_icon;
gbitmap_init_with_resource(check_icon, RESOURCE_ID_ACTION_BAR_ICON_CHECK);
GBitmap *x_icon = &data->x_icon;
gbitmap_init_with_resource(x_icon, RESOURCE_ID_ACTION_BAR_ICON_X);
// Initialize and configure the worker crash dialog's action bar
ActionBarLayer *action_bar = &data->action_bar;
action_bar_layer_init(action_bar);
action_bar_layer_set_icon(action_bar, BUTTON_ID_UP, check_icon);
action_bar_layer_set_icon(action_bar, BUTTON_ID_DOWN, x_icon);
action_bar_layer_set_click_config_provider(action_bar, prv_worker_crash_click_config_provider);
action_bar_layer_set_context(action_bar, data);
// crash_reason buffer is allocated on the heap and freed below
char *crash_reason = prv_create_worker_crash_reason_string(app_install_id);
// Create and configure the worker crash actionable dialog
data->actionable_dialog = actionable_dialog_create("Crashed");
if (!data->actionable_dialog) {
// Just return and don't show any crash UI if we failed to create the actionable dialog
return;
}
Dialog *dialog = actionable_dialog_get_dialog(data->actionable_dialog);
prv_configure_crash_dialog(dialog, crash_reason);
kernel_free(crash_reason);
actionable_dialog_set_action_bar_type(data->actionable_dialog,
DialogActionBarCustom,
action_bar);
DialogCallbacks callbacks = (DialogCallbacks) {
.unload = prv_worker_crash_dialog_unload
};
dialog_set_callbacks(dialog, &callbacks, data);
// Push the worker crash actionable dialog
actionable_dialog_push(data->actionable_dialog, prv_get_window_stack());
light_enable_interaction();
}
void crashed_ui_show_worker_crash(const AppInstallId install_id) {
launcher_task_add_callback(prv_push_worker_crash_dialog, (void *) install_id);
}
// ---------------------------------------------------------------------------
#if (defined(SHOW_BAD_BT_STATE_ALERT) || defined(SHOW_PEBBLE_JUST_RESET_ALERT))
#define YOUR_PEBBLE_RESET \
i18n_noop("Your Pebble just reset. " \
"Please report this using the 'Support' link in the Pebble phone app.")
#define PHONE_BT_CONTROLLER_WEDGED \
i18n_noop("Bluetooth on your phone is in a high power state. " \
"Please report this using 'Support' and reboot your phone.")
//! Display a dialog for watch reset or bluetooth being stuck.
static void prv_push_reset_dialog(void *context) {
const char *crash_reason = context;
ExpandableDialog *expandable_dialog = expandable_dialog_create("Reset");
prv_configure_crash_dialog(expandable_dialog_get_dialog(expandable_dialog), crash_reason);
expandable_dialog_show_action_bar(expandable_dialog, false);
expandable_dialog_push(expandable_dialog, prv_get_window_stack());
light_enable_interaction();
}
//! Restrict only to the two defines above
//! Show the "Your pebble has just reset"
void crashed_ui_show_pebble_reset(void) {
launcher_task_add_callback(prv_push_reset_dialog, YOUR_PEBBLE_RESET);
}
//! Restrict only to the two defines above
//! Show the "Your Bluetooth is ..."
void crashed_ui_show_bluetooth_stuck(void) {
launcher_task_add_callback(prv_push_reset_dialog, PHONE_BT_CONTROLLER_WEDGED);
}
#endif

View file

@ -0,0 +1,27 @@
/*
* 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 "process_management/app_install_types.h"
//! Show the "Your Pebble just reset" modal
void crashed_ui_show_pebble_reset(void);
//! Show the "Bluetooth stuck in ..." modal
void crashed_ui_show_bluetooth_stuck(void);
void crashed_ui_show_worker_crash(const AppInstallId install_id);

View file

@ -0,0 +1,124 @@
/*
* 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 "health_tracking_ui.h"
#include "applib/ui/dialogs/expandable_dialog.h"
#include "applib/ui/window_stack.h"
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "kernel/ui/modals/modal_manager.h"
#include "process_management/app_manager.h"
#include "resource/resource_ids.auto.h"
#include "services/common/i18n/i18n.h"
#include "services/common/light.h"
#include <stdio.h>
#if CAPABILITY_HAS_HEALTH_TRACKING
typedef struct HealthTrackingUIData {
uint32_t res_id;
const char *text;
bool show_action_bar;
} HealthTrackingUIData;
static AppInstallId s_last_app_id;
// ---------------------------------------------------------------------------
static WindowStack *prv_get_window_stack(void) {
return modal_manager_get_window_stack(ModalPriorityAlert);
}
// ---------------------------------------------------------------------------
static void prv_select_handler(ClickRecognizerRef recognizer, void *context) {
ExpandableDialog *expandable_dialog = context;
expandable_dialog_pop(expandable_dialog);
}
// ---------------------------------------------------------------------------
static void prv_push_enable_in_mobile_dialog(void *context) {
HealthTrackingUIData *data = context;
ExpandableDialog *expandable_dialog = expandable_dialog_create("Health Disabled");
Dialog *dialog = expandable_dialog_get_dialog(expandable_dialog);
// Set the base dialog properties
dialog_set_text(dialog, i18n_get(data->text, dialog));
// i18n_free(data->text, dialog);
dialog_set_icon(dialog, data->res_id);
dialog_set_vibe(dialog, false);
// Set the expandable dialog properties
expandable_dialog_show_action_bar(expandable_dialog, data->show_action_bar);
if (data->show_action_bar) {
expandable_dialog_set_select_action(expandable_dialog, RESOURCE_ID_ACTION_BAR_ICON_CHECK,
prv_select_handler);
}
expandable_dialog_push(expandable_dialog, prv_get_window_stack());
light_enable_interaction();
kernel_free(data);
}
// ---------------------------------------------------------------------------
void health_tracking_ui_show_message(uint32_t res_id, const char *text, bool show_action_bar) {
HealthTrackingUIData *data = kernel_malloc(sizeof(HealthTrackingUIData));
*data = (HealthTrackingUIData){
.res_id = res_id,
.text = text,
.show_action_bar = show_action_bar,
};
launcher_task_add_callback(prv_push_enable_in_mobile_dialog, data);
}
// ---------------------------------------------------------------------------
void health_tracking_ui_app_show_disabled(void) {
// Show at most once per app launch
AppInstallId app_id = app_manager_get_current_app_id();
if (app_id == s_last_app_id) {
return;
}
s_last_app_id = app_id;
/// Health disabled dialog
static const char *msg =
i18n_noop("This app requires Pebble Health to work. Enable Health in the Pebble"
" mobile app to continue.");
health_tracking_ui_show_message(RESOURCE_ID_GENERIC_WARNING_TINY, msg, false);
}
// ---------------------------------------------------------------------------
void health_tracking_ui_feature_show_disabled(void) {
/// Feature requires health dialog
static const char *msg =
i18n_noop("This feature requires Pebble Health to work. Enable Health in the Pebble"
" mobile app to continue.");
health_tracking_ui_show_message(RESOURCE_ID_GENERIC_WARNING_TINY, msg, false);
}
// ---------------------------------------------------------------------------
void health_tracking_ui_register_app_launch(AppInstallId app_id) {
s_last_app_id = 0;
}
#endif // CAPABILITY_HAS_HEALTH_TRACKING

View file

@ -0,0 +1,33 @@
/*
* 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 "process_management/app_install_types.h"
//! Show a modal with a message and text with an optional action bar
void health_tracking_ui_show_message(uint32_t res_id, const char *text, bool show_action_bar);
//! Show the modal that tells the user that health tracking is disabled
//! and a given app will not work as expected
void health_tracking_ui_app_show_disabled(void);
//! Show the modal that tells the user that health tracking is disabled
//! and a given feature will not work as expected
void health_tracking_ui_feature_show_disabled(void);
//! Inform the health tracking UI that a new app got launched
void health_tracking_ui_register_app_launch(AppInstallId app_id);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,50 @@
/*
* 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 <stdbool.h>
#include <stdint.h>
#include "applib/fonts/fonts.h"
#include "services/normal/notifications/notifications.h"
#include "kernel/events.h"
void notification_window_service_init(void);
void notification_window_init(bool is_modal);
void notification_window_show(void);
bool notification_window_is_modal(void);
void notification_window_handle_notification(PebbleSysNotificationEvent *e);
void notification_window_handle_reminder(PebbleReminderEvent *e);
void notification_window_handle_dnd_event(PebbleDoNotDisturbEvent *e);
void notification_window_add_notification_by_id(Uuid *id);
void notification_window_focus_notification(Uuid *id, bool animated);
void notification_window_mark_focused_read(void);
void app_notification_window_add_new_notification_by_id(Uuid *id);
void app_notification_window_remove_notification_by_id(Uuid *id);
void app_notification_window_handle_notification_acted_upon_by_id(Uuid *id);

View file

@ -0,0 +1,64 @@
/*
* 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/ui/action_menu_window.h"
#include "applib/ui/status_bar_layer.h"
#include "apps/system_apps/timeline/peek_layer.h"
#include "services/common/evented_timer.h"
#include "services/normal/timeline/swap_layer.h"
typedef struct NotificationWindowData {
Window window;
RegularTimerInfo reminder_watchdog_timer_id; // Clear stale reminders once a minute
EventedTimerID pop_timer_id; //!< Timer that automatically pops us in case of inactivity.
bool pop_timer_is_final; // true, if pop_timer_id cannot be rescheduled anymore
bool is_modal;
bool window_frozen; // Don't pop when performing an action via a hotkey until the action completes
bool first_notif_loaded;
// Used to keep track of when a notification is modified from a different (event)
// task, so the reload only occurs in the correct task when something changes
bool notifications_modified;
// nothing but rendering the action button
Layer action_button_layer;
Uuid notification_app_id; //!< app id for loading custom notification icons
PeekLayer *peek_layer;
TimelineResourceInfo peek_icon_info;
EventedTimerID peek_layer_timer;
Animation *peek_animation;
// Handles the multiple layers
SwapLayer swap_layer;
StatusBarLayer status_layer;
ActionMenu *action_menu;
// Icon in status bar if in DND.
// This should really be part of the status bar but support hasn't been
// implemented yet. This also won't work well with round displays.
// Remove this once the status bar layer supports icons
// PBL-22859
Layer dnd_icon_layer;
GBitmap dnd_icon;
bool dnd_icon_visible;
} NotificationWindowData;

View file

@ -0,0 +1,181 @@
/*
* 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 "notifications_presented_list.h"
#include "util/list.h"
#include "kernel/pbl_malloc.h"
#include <stdbool.h>
// currently focused notification id
static NotifList *s_current_notif;
// List contains all currently presented notifications
// The UI can access only notifications of this list
static NotifList *s_presented_notifs;
static bool prv_filter_presented_notification_by_id(ListNode *found_node, void *data) {
return uuid_equal(&((NotifList*)found_node)->notif.id, (Uuid *)data);
}
static NotifList *prv_find_listnode_for_notif(Uuid *id) {
return (NotifList*) list_find((ListNode *) s_presented_notifs,
prv_filter_presented_notification_by_id, id);
}
Uuid *notifications_presented_list_first(void) {
NotifList * node = (NotifList *)list_get_head((ListNode*)s_presented_notifs);
return node ? &node->notif.id : NULL;
}
Uuid *notifications_presented_list_last(void) {
NotifList * node = (NotifList *)list_get_tail((ListNode*)s_presented_notifs);
return node ? &node->notif.id : NULL;
}
Uuid *notifications_presented_list_relative(Uuid *id, int offset) {
NotifList *const start_node = prv_find_listnode_for_notif(id);
NotifList *const end_node = (NotifList *)list_get_at((ListNode *)start_node, offset);
return end_node ? &end_node->notif.id : NULL;
}
int notifications_presented_list_count(void) {
return list_count((ListNode*)s_presented_notifs);
}
void notifications_presented_list_remove(Uuid *id) {
NotifList *node = prv_find_listnode_for_notif(id);
if (!node) {
return;
}
if (node == s_current_notif) {
NotifList *prev_node = (NotifList *)list_get_prev((ListNode *)s_current_notif);
NotifList *next_node = (NotifList *)list_get_next((ListNode *)s_current_notif);
// If a notification gets removed, we want to show an older notification next as we assume
// that the user scrolls down in the list starting from the newest notification
if (next_node) {
s_current_notif = next_node;
} else {
s_current_notif = prev_node;
}
}
list_remove((ListNode*)node, (ListNode**)&s_presented_notifs, NULL);
task_free(node);
}
static NotifList* prv_add_notification_common(Uuid *id, NotificationType type) {
notifications_presented_list_remove(id);
NotifList *new_entry = task_malloc_check(sizeof(NotifList));
list_init((ListNode*)new_entry);
new_entry->notif.type = type;
new_entry->notif.id = *id;
return new_entry;
}
void notifications_presented_list_add(Uuid *id, NotificationType type) {
NotifList *new_entry = prv_add_notification_common(id, type);
s_presented_notifs = (NotifList *)
list_prepend(&s_presented_notifs->list_node, &new_entry->list_node);
}
void notifications_presented_list_add_sorted(Uuid *id, NotificationType type,
Comparator comparator, bool ascending) {
NotifList *new_entry = prv_add_notification_common(id, type);
s_presented_notifs = (NotifList *) list_sorted_add(&s_presented_notifs->list_node,
&new_entry->list_node, comparator, ascending);
}
NotificationType notifications_presented_list_get_type(Uuid *id) {
NotifList *node = prv_find_listnode_for_notif(id);
if (!node) {
return NotificationInvalid;
}
return node->notif.type;
}
bool notifications_presented_list_set_current(Uuid *id) {
NotifList *node = prv_find_listnode_for_notif(id);
if (!node) {
return false;
}
s_current_notif = node;
return true;
}
Uuid *notifications_presented_list_current(void) {
if (!s_current_notif) {
return NULL;
}
return &s_current_notif->notif.id;
}
Uuid *notifications_presented_list_next(void) {
NotifList *next_node = (NotifList *)list_get_next((ListNode *)s_current_notif);
if (!next_node) {
return NULL;
}
return &next_node->notif.id;
}
int notifications_presented_list_current_idx(void) {
Uuid *id = notifications_presented_list_current();
if (uuid_is_invalid(id)) {
return -1;
}
return list_count_to_head_from((ListNode*) prv_find_listnode_for_notif(id)) - 1;
}
void notifications_presented_list_init(void) {
s_current_notif = NULL;
s_presented_notifs = NULL;
}
void notifications_presented_list_deinit(NotificationListEachCallback callback, void *cb_data) {
s_current_notif = NULL;
while (s_presented_notifs) {
NotifList *head = s_presented_notifs;
list_remove((ListNode *)head, (ListNode **)&s_presented_notifs, NULL);
if (callback) {
callback(&head->notif.id, head->notif.type, cb_data);
}
task_free(head);
}
}
void notifications_presented_list_each(NotificationListEachCallback callback, void *cb_data) {
if (!callback) {
return;
}
NotifList *itr = s_presented_notifs;
while (itr) {
NotifList *next = (NotifList *)itr->list_node.next;
callback(&itr->notif.id, itr->notif.type, cb_data);
itr = next;
}
}

View file

@ -0,0 +1,84 @@
/*
* 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 "util/uuid.h"
#include "services/normal/notifications/notification_types.h"
#include <inttypes.h>
//! @file notifications_presented_list.h
//!
//! \brief File that manages a list of presented notifications.
typedef struct {
ListNode list_node;
NotificationInfo notif;
} NotifList;
//! Get the first notification ID in the presented list
Uuid *notifications_presented_list_first(void);
//! Get the last notification ID in the presented list
Uuid *notifications_presented_list_last(void);
//! Get the notification ID that has the given relative offset from the given id
Uuid *notifications_presented_list_relative(Uuid *id, int offset);
//! Get the currently presented notification in the list
Uuid *notifications_presented_list_current(void);
//! Get the next notification in the list
Uuid *notifications_presented_list_next(void);
//! Set the current notification in the presented list (user scrolled, new notif, etc)
bool notifications_presented_list_set_current(Uuid *id);
//! Remove the given notification from the presented list
void notifications_presented_list_remove(Uuid *id);
//! Add the given notification to the presented list
void notifications_presented_list_add(Uuid *id, NotificationType type);
//! Add the given notification to the presented list
//! The comparator will have to compare two NotifList*
void notifications_presented_list_add_sorted(Uuid *id, NotificationType type,
Comparator comparator, bool ascending);
//! Get the type of the given notification
NotificationType notifications_presented_list_get_type(Uuid *id);
//! Get the count of notifications in the presented list
int notifications_presented_list_count(void);
//! Get the current index (integer based) of the current notification in the presented list
//! This is used for the status bar (ex. "2/5")
int notifications_presented_list_current_idx(void);
//! Inits the notification presented list
void notifications_presented_list_init(void);
typedef void (*NotificationListEachCallback)(Uuid *id, NotificationType type, void *cb_data);
//! Executes the specified callback for each notificaiton in the presented list
//! @param callback If null this function is a no-op
void notifications_presented_list_each(NotificationListEachCallback callback, void *cb_data);
//! Deinits the notification presented list
//! @param callback - If non-null, notifies the caller what item is being removed.
//! The callback routine should not try to modify the notification list
void notifications_presented_list_deinit(NotificationListEachCallback callback, void *cb_data);

View file

@ -0,0 +1,106 @@
/*
* 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 "phone_formatting.h"
#include "applib/graphics/utf8.h"
#include "util/math.h"
#include <ctype.h>
#include <stdio.h>
#include <string.h>
// Turn every word after the first one into an initial.
// e.g. Katharine Claire Berry -> Katharine C. B.
void phone_format_caller_name(const char *full_name, char *destination, size_t length) {
char *space = strchr(full_name, ' ');
// If there are no spaces, just use the whole thing and bail.
if (!space) {
strncpy(destination, full_name, length);
destination[length - 1] = '\0';
return;
}
// Copy the first name to the destination, as long as it fits.
size_t pos = MIN((size_t)(space - full_name), length - 1);
strncpy(destination, full_name, pos);
// Then keep adding the character after the space until we run out of spaces.
do {
space++;
// Abort if it is impossible for us to fit a space, one-byte initial, period and null in the
// buffer (= four bytes)
// Also bail out if this space terminates the string; otherwise we append an unnecessary
// space to the destination.
if ((pos + 4 > length - 1) || (*space == '\0')) {
break;
}
// Skip ahead if this is a space. This avoids stray dots on double spaces.
if (*space == ' ') {
continue;
}
destination[pos++] = ' ';
size_t initial_size = utf8_copy_character((utf8_t *)&destination[pos], (utf8_t *)space,
length - pos - 2); // 2 = ".\0"
// If we couldn't fit anything, stop here.
if (initial_size == 0) {
pos--; // the space we previously added should be omitted from our string.
break;
}
pos += initial_size;
destination[pos++] = '.';
} while ((space = strchr(space, ' ')));
destination[pos] = '\0';
}
// based on https://en.wikipedia.org/wiki/National_conventions_for_writing_telephone_numbers
void phone_format_phone_number(const char *phone_number, char *formatted_phone_number,
size_t length) {
const int phone_number_length = strlen(phone_number);
// Only modify if phone number includes area code and correctly formatted
const int long_distance_min_len = 12; // 650-777-1234, +49 30 90260
if (phone_number_length >= long_distance_min_len) {
int local_number_length = 0;
// Parse from the end of the string to identify the local portion of the phone number
// After local_number_min_len, a separator delimits the regional or international portion
const int local_number_min_len = 6;
for (local_number_length = local_number_min_len; local_number_length < phone_number_length;
local_number_length++) {
const char key = phone_number[phone_number_length - local_number_length - 1];
if (!isdigit(key)) {
break;
}
}
// Force the local part of the phone number to the second line using newline
const int region_min_len = 3;
if (local_number_length <= (phone_number_length - region_min_len)) {
int region_length = phone_number_length - local_number_length;
const char *local_number = &phone_number[region_length];
// Remove dash, dot or space from region line
if ((phone_number[region_length - 1] == '-') || (phone_number[region_length - 1] == '.') ||
(phone_number[region_length - 1] == ' ')) {
region_length--;
}
snprintf(formatted_phone_number, length, "%.*s\n%.*s",
region_length, phone_number, local_number_length, local_number);
return;
}
}
// copy original to the output buffer for non-covered cases
strncpy(formatted_phone_number, phone_number, length);
}

View file

@ -0,0 +1,39 @@
/*
* 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 <stddef.h>
#include <stdint.h>
#include <stdbool.h>
//! Turns every word after the first one into an initial.
//! e.g. Katharine Claire Berry -> Katharine C. B.
//! @param full_name String containing the full name
//! @param destination buffer to copy the abbreviated name into.
//! @param length Size of the destination buffer.
void phone_format_caller_name(const char *full_name, char *destination, size_t length);
//! Forces 2 line formatting on international phone numbers as well as
//! most long distance phone numbers (where required by format and length)
//! e.g. +55 408-555-1212 becomes
//! +55 408
//! 555-1212
//! @param phone_number String containing original phone number
//! @param destination buffer to copy the formatted phone number into.
//! @param length Size of the destination buffer.
void phone_format_phone_number(const char *phone_number, char *formatted_phone_number,
size_t length);

1242
src/fw/popups/phone_ui.c Normal file

File diff suppressed because it is too large Load diff

37
src/fw/popups/phone_ui.h Normal file
View file

@ -0,0 +1,37 @@
/*
* 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 "services/normal/phone_call_util.h"
#include <stdbool.h>
void phone_ui_handle_incoming_call(PebblePhoneCaller *caller, bool can_answer,
bool show_ongoing_call_ui, PhoneCallSource source);
void phone_ui_handle_outgoing_call(PebblePhoneCaller *caller);
void phone_ui_handle_missed_call(void);
void phone_ui_handle_call_start(bool can_decline);
void phone_ui_handle_call_end(bool call_accepted, bool disconnected);
void phone_ui_handle_call_hide(void);
void phone_ui_handle_caller_id(PebblePhoneCaller *caller);

View file

@ -0,0 +1,135 @@
/*
* 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 "switch_worker_ui.h"
#include <stdio.h>
#include <string.h>
#include "applib/ui/action_bar_layer.h"
#include "applib/ui/dialogs/confirmation_dialog.h"
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "kernel/ui/modals/modal_manager.h"
#include "process_management/app_install_manager.h"
#include "process_management/process_manager.h"
#include "process_management/worker_manager.h"
#include "services/common/i18n/i18n.h"
#include "services/normal/app_cache.h"
typedef struct {
AppInstallId new_worker_id;
bool set_as_default;
ConfirmationDialog *confirmation_dialog;
} SwitchWorkerUIArgs;
static bool s_is_on_screen = false;
static void prv_click_confirm_decline_callback(ClickRecognizerRef recognizer, void *context) {
// TODO: Currently set_as_default does nothing. This will be corrected later on to allow
// launching of a worker while an app is open, then returning to the default worker after
// the application has been exited. The likely UI flow would prompt the user to set the
// worker as the default (if the flag is false) after they've confirmed enabling activity
// tracking using the launch application, to which they can decline.
SwitchWorkerUIArgs *args = (SwitchWorkerUIArgs *) context;
ConfirmationDialog *confirmation_dialog = args->confirmation_dialog;
confirmation_dialog_pop(confirmation_dialog);
bool selection_confirmed = (click_recognizer_get_button_id(recognizer) == BUTTON_ID_UP);
if (selection_confirmed) {
if (!app_cache_entry_exists(args->new_worker_id)) {
// If an app cache entry does not exist for the new worker, then we will have to
// fetch the application. Since this will prompt the user to confirm activity tracking
// for the worker because the previous worker is still running, we have to kill the
// previous worker here.
process_manager_put_kill_process_event(PebbleTask_Worker, true /* graceful */);
}
worker_manager_set_default_install_id(args->new_worker_id);
worker_manager_put_launch_worker_event(args->new_worker_id);
}
s_is_on_screen = false;
task_free(args);
}
static void prv_click_config_provider(void *context) {
window_single_click_subscribe(BUTTON_ID_UP, prv_click_confirm_decline_callback);
window_single_click_subscribe(BUTTON_ID_DOWN, prv_click_confirm_decline_callback);
window_single_click_subscribe(BUTTON_ID_BACK, prv_click_confirm_decline_callback);
}
void switch_worker_confirm(AppInstallId new_worker_id, bool set_as_default,
WindowStack *window_stack) {
AppInstallId cur_worker_id = worker_manager_get_current_worker_id();
if (s_is_on_screen == true) {
// If we already have a window up, let that one finish. This prevents apps that
// spam worker launch from displaying multiple confirmation dialogs on-top of
// one another.
return;
}
if (cur_worker_id == INSTALL_ID_INVALID) {
// If there is no worker running, we can simply launch the new one without confirming
worker_manager_put_launch_worker_event(new_worker_id);
return;
} else if (cur_worker_id == new_worker_id) {
// Or if the new one is already running, then there is nothing to do
return;
}
AppInstallEntry old_entry;
if (!app_install_get_entry_for_install_id(cur_worker_id, &old_entry)) {
return;
}
AppInstallEntry new_entry;
if (!app_install_get_entry_for_install_id(new_worker_id, &new_entry)) {
return;
}
s_is_on_screen = true;
ConfirmationDialog *confirmation_dialog = confirmation_dialog_create("Background App");
Dialog *dialog = confirmation_dialog_get_dialog(confirmation_dialog);
const char *fmt = i18n_get("Run %s instead of %s as the background app?", confirmation_dialog);
char *msg_buffer = task_zalloc_check(DIALOG_MAX_MESSAGE_LEN);
sniprintf(msg_buffer, DIALOG_MAX_MESSAGE_LEN, fmt, new_entry.name, old_entry.name);
confirmation_dialog_set_click_config_provider(confirmation_dialog, prv_click_config_provider);
dialog_set_background_color(dialog, GColorCobaltBlue);
dialog_set_text_color(dialog, GColorWhite);
dialog_set_text(dialog, msg_buffer);
task_free(msg_buffer);
i18n_free_all(confirmation_dialog);
SwitchWorkerUIArgs *args = task_malloc_check(sizeof(SwitchWorkerUIArgs));
*args = (SwitchWorkerUIArgs) {
.new_worker_id = new_worker_id,
.set_as_default = set_as_default,
.confirmation_dialog = confirmation_dialog,
};
// Set our arguments to be passed as the context to the confirmation dialog action bar
ActionBarLayer *action_bar = confirmation_dialog_get_action_bar(confirmation_dialog);
action_bar_layer_set_context(action_bar, args);
confirmation_dialog_push(confirmation_dialog, window_stack);
}

View file

@ -0,0 +1,29 @@
/*
* 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/ui/window_stack.h"
#include "kernel/events.h"
//! @param new_worker_id The new ID that we'd like to ask the user to switch to
//! @param set_as_default Whether this new worker should become the default after being accepted
//! @param window_stack Which window stack to push the dialog to
//! @param exit_callback Callback to be called on dialog pop (may be NULL if not used)
//! @param callback_context Context which is passed to provided callbacks
void switch_worker_confirm(AppInstallId new_worker_id, bool set_as_default,
WindowStack *window_stack);

View file

@ -0,0 +1,575 @@
/*
* 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 "peek_private.h"
#include "applib/ui/property_animation.h"
#include "applib/ui/window_stack.h"
#include "applib/unobstructed_area_service.h"
#include "apps/system_apps/timeline/timeline_common.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 "shell/prefs.h"
#include "syscall/syscall_internal.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/size.h"
#include "util/struct.h"
#include <pebbleos/cron.h>
#define TIMELINE_PEEK_FRAME_HIDDEN GRect(0, DISP_ROWS, DISP_COLS, TIMELINE_PEEK_HEIGHT)
#define TIMELINE_PEEK_OUTER_BORDER_WIDTH PBL_IF_RECT_ELSE(2, 1)
#define TIMELINE_PEEK_MULTI_BORDER_WIDTH (1)
#define TIMELINE_PEEK_MULTI_CONTENT_HEIGHT PBL_IF_RECT_ELSE(2, 1)
#define TIMELINE_PEEK_MAX_CONCURRENT (2)
static TimelinePeek s_peek;
static unsigned int prv_get_concurrent_height(unsigned int num_concurrent) {
// Height of the border and other concurrent contents
return (TIMELINE_PEEK_OUTER_BORDER_WIDTH +
(num_concurrent * (TIMELINE_PEEK_MULTI_BORDER_WIDTH +
TIMELINE_PEEK_MULTI_CONTENT_HEIGHT)));
}
unsigned int timeline_peek_get_concurrent_height(unsigned int num_concurrent) {
return prv_get_concurrent_height(MIN(num_concurrent, TIMELINE_PEEK_MAX_CONCURRENT));
}
static void prv_draw_background(GContext *ctx, const GRect *frame_orig,
unsigned int num_concurrent) {
GRect frame = *frame_orig;
#if PBL_RECT
// Fill all the way to the bottom of the screen
frame.size.h = DISP_ROWS - frame.origin.y;
#endif
const GColor background_color = GColorWhite;
graphics_context_set_fill_color(ctx, background_color);
graphics_fill_rect(ctx, &frame);
// Draw the icon background
frame.origin.x += DISP_COLS - TIMELINE_PEEK_ICON_BOX_WIDTH;
frame.size.w = TIMELINE_PEEK_ICON_BOX_WIDTH;
graphics_context_set_fill_color(ctx, TIMELINE_FUTURE_COLOR);
graphics_fill_rect(ctx, &frame);
// Draw the top border and concurrent event indicators
frame = *frame_orig;
const GColor border_color = GColorBlack;
for (unsigned int i = 0; i <= num_concurrent; i++) {
const bool has_content = (i < num_concurrent);
for (unsigned int type = 0; type < (has_content ? 2 : 1); type++) {
const bool is_outer = (i == 0);
const bool is_border = (type == 0);
const int height = (is_outer && is_border) ? TIMELINE_PEEK_OUTER_BORDER_WIDTH :
is_border ? TIMELINE_PEEK_MULTI_BORDER_WIDTH :
TIMELINE_PEEK_MULTI_CONTENT_HEIGHT;
frame.size.h = height;
graphics_context_set_fill_color(ctx, is_border ? border_color : background_color);
graphics_fill_rect(ctx, &frame);
frame.origin.y += height;
}
}
#if PBL_ROUND
// Draw the bottom border
frame = *frame_orig;
frame.origin.y += frame.size.h - TIMELINE_PEEK_OUTER_BORDER_WIDTH;
frame.size.h = TIMELINE_PEEK_OUTER_BORDER_WIDTH;
graphics_context_set_fill_color(ctx, border_color);
graphics_fill_rect(ctx, &frame);
#endif
}
void timeline_peek_draw_background(GContext *ctx, const GRect *frame,
unsigned int num_concurrent) {
prv_draw_background(ctx, frame, num_concurrent);
}
static void prv_timeline_peek_update_proc(Layer *layer, GContext *ctx) {
TimelinePeek *peek = (TimelinePeek *)layer;
const unsigned int num_concurrent = peek->peek_layout ?
MIN(peek->peek_layout->info.num_concurrent, TIMELINE_PEEK_MAX_CONCURRENT) : 0;
if (peek->removing_concurrent && (num_concurrent > 0)) {
prv_draw_background(ctx, &TIMELINE_PEEK_FRAME_VISIBLE, num_concurrent - 1);
}
prv_draw_background(ctx, &peek->layout_layer.frame, num_concurrent);
}
static void prv_redraw(void *UNUSED data) {
TimelinePeek *peek = &s_peek;
layer_mark_dirty(&peek->layout_layer);
}
static void prv_cron_callback(CronJob *job, void *UNUSED data) {
launcher_task_add_callback(prv_redraw, NULL);
cron_job_schedule(job);
}
static CronJob s_timeline_peek_job = {
.minute = CRON_MINUTE_ANY,
.hour = CRON_HOUR_ANY,
.mday = CRON_MDAY_ANY,
.month = CRON_MONTH_ANY,
.cb = prv_cron_callback,
};
static void prv_destroy_layout(void) {
TimelinePeek *peek = &s_peek;
if (!peek->peek_layout) {
return;
}
layout_destroy(&peek->peek_layout->timeline_layout->layout_layer);
timeline_item_destroy(peek->peek_layout->item);
task_free(peek->peek_layout);
peek->peek_layout = NULL;
}
static PeekLayout *prv_create_layout(TimelineItem *item, unsigned int num_concurrent) {
PeekLayout *layout = task_zalloc_check(sizeof(PeekLayout));
item = timeline_item_copy(item);
layout->item = item;
timeline_layout_init_info(&layout->info, item, time_util_get_midnight_of(rtc_get_time()));
layout->info.num_concurrent = num_concurrent;
const LayoutLayerConfig config = {
.frame = &GRect(0, 0, DISP_COLS, TIMELINE_PEEK_HEIGHT),
.attributes = &item->attr_list,
.mode = LayoutLayerModePeek,
.app_id = &item->header.parent_id,
.context = &layout->info,
};
layout->timeline_layout = (TimelineLayout *)layout_create(item->header.layout, &config);
return layout;
}
static void prv_set_layout(PeekLayout *layout) {
prv_destroy_layout();
TimelinePeek *peek = &s_peek;
peek->peek_layout = layout;
layer_add_child(&peek->layout_layer, &peek->peek_layout->timeline_layout->layout_layer.layer);
}
static void prv_unschedule_animation(TimelinePeek *peek) {
animation_unschedule(peek->animation);
peek->animation = NULL;
}
static bool prv_should_use_unobstructed_area() {
GSize app_framebuffer_size;
app_manager_get_framebuffer_size(&app_framebuffer_size);
return (DISP_ROWS - app_framebuffer_size.h) < TIMELINE_PEEK_HEIGHT;
}
static void prv_peek_frame_setup(Animation *animation) {
PropertyAnimation *prop_anim = (PropertyAnimation *)animation;
TimelinePeek *peek;
property_animation_subject(prop_anim, (void *)&peek, false /* set */);
GRect from_frame;
property_animation_get_from_grect(prop_anim, &from_frame);
GRect to_frame;
property_animation_get_to_grect(prop_anim, &to_frame);
if (prv_should_use_unobstructed_area()) {
unobstructed_area_service_will_change(from_frame.origin.y, to_frame.origin.y);
}
}
static void prv_peek_frame_update(Animation *animation, AnimationProgress progress) {
PropertyAnimation *prop_anim = (PropertyAnimation *)animation;
property_animation_update_grect(prop_anim, progress);
TimelinePeek *peek;
property_animation_subject(prop_anim, (void *)&peek, false /* set */);
GRect to_frame;
property_animation_get_to_grect(prop_anim, &to_frame);
if (prv_should_use_unobstructed_area()) {
unobstructed_area_service_change(peek->layout_layer.frame.origin.y, to_frame.origin.y,
progress);
}
}
static void prv_peek_frame_teardown(Animation *animation) {
PropertyAnimation *prop_anim = (PropertyAnimation *)animation;
GRect to_frame;
property_animation_get_to_grect(prop_anim, &to_frame);
if (prv_should_use_unobstructed_area()) {
unobstructed_area_service_did_change(to_frame.origin.y);
}
}
static GRect prv_peek_frame_getter(void *subject) {
TimelinePeek *peek = subject;
GRect frame;
layer_get_frame(&peek->layout_layer, &frame);
return frame;
}
static void prv_peek_frame_setter(void *subject, GRect frame) {
TimelinePeek *peek = subject;
layer_set_frame(&peek->layout_layer, &frame);
}
static const PropertyAnimationImplementation s_peek_prop_impl = {
.base = {
.setup = prv_peek_frame_setup,
.update = prv_peek_frame_update,
.teardown = prv_peek_frame_teardown,
},
.accessors = {
.getter.grect = prv_peek_frame_getter,
.setter.grect = prv_peek_frame_setter,
},
};
static void prv_peek_anim_stopped(Animation *animation, bool finished, void *context) {
TimelinePeek *peek = &s_peek;
if (context) {
// Replace the previous item with the next item
PeekLayout *layout = context;
prv_set_layout(layout);
// Reset the frame
layer_set_frame(&peek->layout_layer, &TIMELINE_PEEK_FRAME_VISIBLE);
} else if (!peek->visible) {
// If the peek was becoming hidden, destroy the timeline layout
prv_destroy_layout();
}
peek->removing_concurrent = false;
}
static const AnimationHandlers s_peek_anim_handlers = {
.stopped = prv_peek_anim_stopped,
};
static void prv_transition_frame(TimelinePeek *peek, bool visible, bool animated) {
prv_unschedule_animation(peek);
const bool last_visible = peek->visible;
peek->visible = visible;
GRect to_frame = visible ? TIMELINE_PEEK_FRAME_VISIBLE : TIMELINE_PEEK_FRAME_HIDDEN;
if ((last_visible == visible) && grect_equal(&peek->layout_layer.frame, &to_frame)) {
// No change
return;
}
if (!animated) {
layer_set_frame(&peek->layout_layer, &to_frame);
return;
}
PropertyAnimation *prop_anim = property_animation_create(&s_peek_prop_impl, peek, NULL, NULL);
property_animation_set_from_grect(prop_anim, &peek->layout_layer.frame);
property_animation_set_to_grect(prop_anim, &to_frame);
Animation *animation = property_animation_get_animation(prop_anim);
animation_set_duration(animation, interpolate_moook_duration());
animation_set_custom_interpolation(animation, interpolate_moook);
animation_set_handlers(animation, s_peek_anim_handlers, NULL);
peek->animation = animation;
animation_schedule(animation);
}
#define EXTENDED_BOUNCE_BACK (2 * INTERPOLATE_MOOOK_BOUNCE_BACK)
static const int32_t s_extended_moook_out[] =
{EXTENDED_BOUNCE_BACK, INTERPOLATE_MOOOK_BOUNCE_BACK, 2, 1, 0};
static const MoookConfig s_extended_moook_out_config = {
.frames_out = s_extended_moook_out,
.num_frames_out = ARRAY_LENGTH(s_extended_moook_out),
.no_bounce_back = true,
};
static int64_t prv_interpolate_extended_moook_out(AnimationProgress progress, int64_t from,
int64_t to) {
return interpolate_moook_custom(progress, from, to, &s_extended_moook_out_config);
}
static Animation *prv_create_transition_adding_concurrent(
TimelinePeek *peek, PeekLayout *layout) {
const int height_shrink = 20;
GRect frame_normal = TIMELINE_PEEK_FRAME_VISIBLE;
GRect frame_shrink = grect_inset(frame_normal, GEdgeInsets(0, 0, height_shrink, 0));
// Starting with shrink instead of ending with it will flash white
PropertyAnimation *white_prop_anim = property_animation_create_layer_frame(
&peek->layout_layer, &frame_shrink, &frame_normal);
Animation *white_animation = property_animation_get_animation(white_prop_anim);
animation_set_duration(white_animation, ANIMATION_TARGET_FRAME_INTERVAL_MS);
animation_set_handlers(white_animation, s_peek_anim_handlers, layout);
GRect frame_bounce =
grect_inset(frame_normal, GEdgeInsets(-EXTENDED_BOUNCE_BACK, 0, 0, 0));
PropertyAnimation *bounce_prop_anim = property_animation_create_layer_frame(
&peek->layout_layer, &frame_bounce, &frame_normal);
Animation *bounce_animation = property_animation_get_animation(bounce_prop_anim);
animation_set_duration(bounce_animation,
interpolate_moook_custom_duration(&s_extended_moook_out_config));
animation_set_custom_interpolation(bounce_animation, prv_interpolate_extended_moook_out);
return animation_sequence_create(white_animation, bounce_animation, NULL);
}
static const int32_t s_custom_moook_in[] = {0, 1, INTERPOLATE_MOOOK_BOUNCE_BACK};
static const MoookConfig s_custom_moook_in_config = {
.frames_in = s_custom_moook_in,
.num_frames_in = ARRAY_LENGTH(s_custom_moook_in),
};
static int64_t prv_interpolate_custom_moook_in(AnimationProgress progress, int64_t from,
int64_t to) {
return interpolate_moook_custom(progress, from, to, &s_custom_moook_in_config);
}
static int64_t prv_interpolate_moook_out(AnimationProgress progress, int64_t from, int64_t to) {
return interpolate_moook_out(progress, from, to, 0 /* num_frames_from */,
false /* bounce_back */);
}
static Animation *prv_create_transition_removing_concurrent(
TimelinePeek *peek, PeekLayout *layout) {
PropertyAnimation *remove_prop_anim = property_animation_create_layer_frame(
&peek->layout_layer, &TIMELINE_PEEK_FRAME_VISIBLE, &TIMELINE_PEEK_FRAME_HIDDEN);
Animation *remove_animation = property_animation_get_animation(remove_prop_anim);
// Cut out the last frame
animation_set_duration(remove_animation,
interpolate_moook_custom_duration(&s_custom_moook_in_config));
animation_set_custom_interpolation(remove_animation, prv_interpolate_custom_moook_in);
animation_set_handlers(remove_animation, s_peek_anim_handlers, layout);
GRect bounds_normal = { .size = TIMELINE_PEEK_FRAME_VISIBLE.size };
GRect bounds_bounce = { .origin.y = TIMELINE_PEEK_HEIGHT, .size = bounds_normal.size };
PropertyAnimation *bounce_prop_anim = property_animation_create_layer_bounds(
&peek->layout_layer, &bounds_bounce, &bounds_normal);
Animation *bounce_animation = property_animation_get_animation(bounce_prop_anim);
animation_set_duration(bounce_animation, interpolate_moook_out_duration());
animation_set_custom_interpolation(bounce_animation, prv_interpolate_moook_out);
return animation_sequence_create(remove_animation, bounce_animation, NULL);
}
static void prv_transition_concurrent(TimelinePeek *peek, PeekLayout *layout) {
const unsigned int old_num_concurrent = peek->peek_layout->info.num_concurrent;
const unsigned int new_num_concurrent = layout->info.num_concurrent;
if (uuid_equal(&peek->peek_layout->item->header.id, &layout->item->header.id) &&
(old_num_concurrent == new_num_concurrent)) {
// Either nothing changed or the item content changed, just set the layout
prv_set_layout(layout);
return;
}
prv_unschedule_animation(peek);
Animation *animation = NULL;
if (peek->peek_layout && (old_num_concurrent < new_num_concurrent)) {
animation = prv_create_transition_adding_concurrent(peek, layout);
} else {
animation = prv_create_transition_removing_concurrent(peek, layout);
peek->removing_concurrent = true;
}
peek->animation = animation;
animation_schedule(animation);
}
static void prv_push_timeline_peek(void *unused) {
timeline_peek_push();
}
void timeline_peek_init(void) {
TimelinePeek *peek = &s_peek;
*peek = (TimelinePeek) {
#if CAPABILITY_HAS_TIMELINE_PEEK && !SHELL_SDK
.enabled = timeline_peek_prefs_get_enabled(),
#endif
};
window_init(&peek->window, WINDOW_NAME("Timeline Peek"));
window_set_focusable(&peek->window, false);
window_set_transparent(&peek->window, true);
layer_set_update_proc(&peek->window.layer, prv_timeline_peek_update_proc);
layer_init(&peek->layout_layer, &TIMELINE_PEEK_FRAME_HIDDEN);
layer_add_child(&peek->window.layer, &peek->layout_layer);
#if CAPABILITY_HAS_TIMELINE_PEEK
timeline_peek_set_show_before_time(timeline_peek_prefs_get_before_time() * SECONDS_PER_MINUTE);
#endif
// Wait one event loop to show the timeline peek
launcher_task_add_callback(prv_push_timeline_peek, NULL);
}
static void prv_set_visible(bool visible, bool animated) {
#if CAPABILITY_HAS_TIMELINE_PEEK
TimelinePeek *peek = &s_peek;
if (!peek->started && visible) {
cron_job_schedule(&s_timeline_peek_job);
} else {
cron_job_unschedule(&s_timeline_peek_job);
}
prv_transition_frame(peek, visible, animated);
#endif
}
static bool prv_can_animate(void) {
return app_manager_is_watchface_running();
}
void timeline_peek_set_visible(bool visible, bool animated) {
TimelinePeek *peek = &s_peek;
#if !SHELL_SDK
if (!peek->exists) {
visible = false;
}
#endif
prv_set_visible((app_manager_is_watchface_running() && peek->enabled && visible),
(prv_can_animate() && animated));
}
void timeline_peek_set_item(TimelineItem *item, bool started, unsigned int num_concurrent,
bool first, bool animated) {
TimelinePeek *peek = &s_peek;
animated = (prv_can_animate() && animated);
if (!animated) {
// We are not animating and thus don't need to retain the layout
prv_destroy_layout();
}
peek->exists = (item != NULL);
peek->started = started;
peek->first = first;
timeline_peek_set_visible(peek->exists, animated);
PeekLayout *layout = item ? prv_create_layout(item, num_concurrent) : NULL;
if (animated && !peek->animation && peek->visible) {
// Swap the layout in an animation
prv_transition_concurrent(peek, layout);
} else if (layout) {
// Immediately set the new layout
prv_set_layout(layout);
}
}
void timeline_peek_dismiss(void) {
TimelinePeek *peek = &s_peek;
if (!peek->peek_layout) {
return;
}
TimelineItem *item = peek->peek_layout->item;
const status_t rv = pin_db_set_status_bits(&item->header.id, TimelineItemStatusDismissed);
if (rv == S_SUCCESS) {
timeline_event_refresh();
} else {
char uuid_buffer[UUID_STRING_BUFFER_LENGTH];
uuid_to_string(&item->header.id, uuid_buffer);
PBL_LOG(LOG_LEVEL_WARNING, "Failed to dismiss Timeline Peek event %s (status: %"PRIi32")",
uuid_buffer, rv);
}
}
int16_t timeline_peek_get_origin_y(void) {
TimelinePeek *peek = &s_peek;
return peek->layout_layer.frame.origin.y;
}
int16_t timeline_peek_get_obstruction_origin_y(void) {
return prv_should_use_unobstructed_area() ? timeline_peek_get_origin_y() : DISP_ROWS;
}
void timeline_peek_get_item_id(TimelineItemId *item_id_out) {
TimelinePeek *peek = &s_peek;
*item_id_out = (peek->enabled && peek->visible && peek->exists && peek->peek_layout)
? peek->peek_layout->item->header.id : UUID_INVALID;
}
bool timeline_peek_is_first_event(void) {
TimelinePeek *peek = &s_peek;
return peek->first;
}
bool timeline_peek_is_future_empty(void) {
TimelinePeek *peek = &s_peek;
return peek->future_empty;
}
void timeline_peek_push(void) {
TimelinePeek *peek = &s_peek;
modal_window_push(&peek->window, ModalPriorityDiscreet, true);
}
void timeline_peek_pop(void) {
TimelinePeek *peek = &s_peek;
window_stack_remove(&peek->window, true);
}
void timeline_peek_set_enabled(bool enabled) {
TimelinePeek *peek = &s_peek;
peek->enabled = enabled;
timeline_peek_set_visible(enabled, true /* animated */);
}
void timeline_peek_handle_peek_event(PebbleTimelinePeekEvent *event) {
#if CAPABILITY_HAS_TIMELINE_PEEK
TimelinePeek *peek = &s_peek;
peek->future_empty = event->is_future_empty;
bool show = false;
bool started = false;
if (event->item_id != NULL) {
switch (event->time_type) {
case TimelinePeekTimeType_None:
case TimelinePeekTimeType_SomeTimeNext:
case TimelinePeekTimeType_WillEnd:
break;
case TimelinePeekTimeType_ShowWillStart:
show = true;
break;
case TimelinePeekTimeType_ShowStarted:
show = true;
started = true;
break;
}
}
TimelineItem item = {};
if (show) {
const status_t rv = pin_db_get(event->item_id, &item);
// We failed to read the pin since it may have been deleted immediately. We will probably
// momentarily recover from another peek event resulting from the delete.
show = (rv == S_SUCCESS);
}
if (show) {
timeline_peek_set_item(&item, started, event->num_concurrent,
event->is_first_event, true /* animated */);
} else {
timeline_peek_set_item(NULL, false /* started */, 0 /* num_concurrent */,
false /* is_first_event */, true /* animated */);
}
timeline_item_free_allocated_buffer(&item);
#endif
}
void timeline_peek_handle_process_start(void) {
#if CAPABILITY_HAS_TIMELINE_PEEK
timeline_peek_set_visible(true, false /* animated */);
#endif
}
void timeline_peek_handle_process_kill(void) {
#if CAPABILITY_HAS_TIMELINE_PEEK
timeline_peek_set_visible(false, false /* animated */);
#endif
}
#if UNITTEST
TimelinePeek *timeline_peek_get_peek(void) {
return &s_peek;
}
#endif

View file

@ -0,0 +1,120 @@
/*
* 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/preferred_content_size.h"
#include "applib/ui/animation.h"
#include "applib/ui/window.h"
#include "kernel/events.h"
#include "services/normal/timeline/timeline.h"
#define TIMELINE_PEEK_HEIGHT \
PREFERRED_CONTENT_SIZE_SWITCH(PreferredContentSizeDefault, \
/* This is the same as Medium until Small is designed */ \
/* small */ PBL_IF_RECT_ELSE(51, 45), \
/* medium */ PBL_IF_RECT_ELSE(51, 45), \
/* large */ 59, \
/* This is the same as Large until ExtraLarge is designed */ \
/* x-large */ 59 \
)
#define TIMELINE_PEEK_ICON_BOX_WIDTH \
PREFERRED_CONTENT_SIZE_SWITCH(PreferredContentSizeDefault, \
/* This is the same as Medium until Small is designed */ \
/* small */ PBL_IF_RECT_ELSE(30, 51), \
/* medium */ PBL_IF_RECT_ELSE(30, 51), \
/* large */ 34, \
/* This is the same as Large until ExtraLarge is designed */ \
/* x-large */ 34 \
)
#define TIMELINE_PEEK_MARGIN (5)
#define TIMELINE_PEEK_ORIGIN_Y_VISIBLE PBL_IF_RECT_ELSE(DISP_ROWS - TIMELINE_PEEK_HEIGHT, 112)
#define TIMELINE_PEEK_FRAME_VISIBLE GRect(0, TIMELINE_PEEK_ORIGIN_Y_VISIBLE, DISP_COLS, \
TIMELINE_PEEK_HEIGHT)
//! Gets the concurrent height needed to render for the number of concurrent events.
//! @return The concurrent height
unsigned int timeline_peek_get_concurrent_height(unsigned int num_concurrent);
//! Draws the timeline peek background.
//! @param ctx Graphics context to draw with.
//! @param frame The rectangle of the peek to draw.
//! @param num_concurrent The number of events to indicate.
void timeline_peek_draw_background(GContext *ctx, const GRect *frame,
unsigned int num_concurrent);
//! Initializes a TimelinePeek overlay (transparent, unfocusable modal window)
void timeline_peek_init(void);
//! Sets whether the peek is visible. The peek will animate in or out depending if it was
//! previously visible or not.
//! @param visible Whether to show the peek
//! @param animated Whether the peek animates into its new visibility state
void timeline_peek_set_visible(bool visible, bool animated);
//! Sets the pin information to display as well as the number of concurrent events
//! @param item TimelineItem reference which is stored and expected to exist until replaced. If
//! NULL, the peek will be emptied and no event information is displayed.
//! @param started Whether the item has started or not.
//! @param num_concurrent The number of concurrent events to indicate
//! @param first Whether the item is the first event in Timeline.
//! @param animated Whether the peek animates into its new visibility state
void timeline_peek_set_item(TimelineItem *item, bool started, unsigned int num_concurrent,
bool first, bool animated);
//! Returns whether the item in the peek is the first event in Timeline.
//! @return true if the peek is showing the first time, false otherwise.
bool timeline_peek_is_first_event(void);
//! Returns whether Timeline future is empty upon entering it.
//! @return true if Timeline future is empty, false otherwise.
bool timeline_peek_is_future_empty(void);
//! Dismisses the current TimelinePeek Timeline item.
void timeline_peek_dismiss(void);
//! Gets the current y of the peek
int16_t timeline_peek_get_origin_y(void);
//! Gets the current obstruction y from which the unobstructed area can be derived from
int16_t timeline_peek_get_obstruction_origin_y(void);
//! Gets the current timeline item id. If there is no item, UUID_INVALID is given instead.
//! @param item_id_out Pointer to the item id buffer to write to.
void timeline_peek_get_item_id(TimelineItemId *item_id_out);
//! Pushes the TimelinePeek window
void timeline_peek_push(void);
//! Pops the TimelinePeek window
void timeline_peek_pop(void);
//! Toggles whether TimelinePeek is enabled. Used by the qemu serial protocol for the SDK.
void timeline_peek_set_enabled(bool enabled);
//! Handles timeline peek events
void timeline_peek_handle_peek_event(PebbleTimelinePeekEvent *event);
//! Handles process start synchronously. This is synchronous because the app manager needs to know
//! the new unobstructed area that would result from process start in order to prepare the app
//! state initialization parameters with the new obstruction position.
void timeline_peek_handle_process_start(void);
//! Handles process kill synchronously. This is synchronous because process start is handled
//! synchronously -- a processing being killed and another process starting happen in sequence.
void timeline_peek_handle_process_kill(void);

View file

@ -0,0 +1,68 @@
/*
* 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 "peek_animations.h"
#include "applib/graphics/gtypes.h"
#include "applib/graphics/graphics.h"
#include "util/size.h"
#define LINE_WIDTH (2)
#define LINE_SPACING (10)
static void prv_draw_vertical_lines(GContext *ctx, unsigned int num_lines,
const uint16_t *offsets_y, const uint16_t *heights,
unsigned int width, unsigned int spacing, GPoint offset) {
for (int i = 0; i < (int)num_lines; i++) {
GRect box = {
.origin = gpoint_add(GPoint((spacing + width) * i - width,
offsets_y ? offsets_y[i] : 0), offset),
.size = { width, heights[i] },
};
graphics_fill_rect(ctx, &box);
}
}
void peek_animations_draw_compositor_foreground_speed_lines(GContext *ctx, GPoint offset) {
static const uint16_t s_upper_heights[] = { 48, 73, 78, 48, 48, 48, 61, 48 };
prv_draw_vertical_lines(ctx, ARRAY_LENGTH(s_upper_heights), NULL /* offsets_y */,
s_upper_heights, LINE_WIDTH, LINE_SPACING, offset);
static const uint16_t s_lower_offsets_y[] = { 24, 24, 0, 19, 7, 0, 0, 24 };
static const uint16_t s_lower_heights[] = { 48, 48, 72, 53, 65, 72, 72, 48 };
offset.y += 90;
prv_draw_vertical_lines(ctx, ARRAY_LENGTH(s_lower_heights), s_lower_offsets_y, s_lower_heights,
LINE_WIDTH, LINE_SPACING, offset);
}
void peek_animations_draw_compositor_background_speed_lines(GContext *ctx, GPoint offset) {
static const uint16_t s_heights[] = { 0, DISP_ROWS, DISP_ROWS, 0, 0, 0, DISP_ROWS };
prv_draw_vertical_lines(ctx, ARRAY_LENGTH(s_heights), NULL /* offsets_y */, s_heights,
LINE_WIDTH, LINE_SPACING, offset);
}
void peek_animations_draw_timeline_speed_lines(GContext *ctx, GPoint offset) {
static const uint16_t s_upper_offsets_y[] = { 12, 0, 0, 12, 12, 12, 12, 12 };
static const uint16_t s_upper_heights[] = { 53, 65, 65, 53, 53, 53, 53, 53 };
prv_draw_vertical_lines(ctx, ARRAY_LENGTH(s_upper_heights), s_upper_offsets_y, s_upper_heights,
LINE_WIDTH, LINE_SPACING, offset);
static const uint16_t s_lower_offsets_y[] = { 5, 5, 0, 0, 5, 5, 5, 5 };
static const uint16_t s_lower_heights[] = { 53, 87, 87, 53, 53, 53, 53, 53 };
offset.y += 65;
prv_draw_vertical_lines(ctx, ARRAY_LENGTH(s_lower_heights), s_lower_offsets_y, s_lower_heights,
LINE_WIDTH, LINE_SPACING, offset);
}

View file

@ -0,0 +1,27 @@
/*
* 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"
#define PEEK_ANIMATIONS_SPEED_LINES_OFFSET_X (7)
void peek_animations_draw_compositor_foreground_speed_lines(GContext *ctx, GPoint offset);
void peek_animations_draw_compositor_background_speed_lines(GContext *ctx, GPoint offset);
void peek_animations_draw_timeline_speed_lines(GContext *ctx, GPoint offset);

View file

@ -0,0 +1,47 @@
/*
* 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 "peek.h"
#include "applib/ui/animation.h"
#include "applib/ui/window.h"
#include "services/normal/timeline/timeline_layout.h"
typedef struct PeekLayout {
TimelineLayoutInfo info;
TimelineLayout *timeline_layout;
TimelineItem *item;
} PeekLayout;
typedef struct TimelinePeek {
Window window;
Layer layout_layer;
PeekLayout *peek_layout;
Animation *animation; //!< Currently running animation
bool exists; //!< Whether there exists an item to show in peek.
bool started; //!< Whether the item has started.
bool enabled; //!< Whether to persistently show or hide the peek.
bool visible; //!< Whether the peek is visible or not.
bool first; //!< Whether the item is the first item in Timeline.
bool removing_concurrent; //!< Whether the removing concurrent animation is occurring.
bool future_empty; //!< Whether Timeline future is empty.
} TimelinePeek;
#if UNITTEST
TimelinePeek *timeline_peek_get_peek(void);
#endif

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 "timeline_item_layer.h"
#include "applib/graphics/graphics.h"
#include "applib/ui/action_menu_window.h"
#include "applib/ui/window.h"
#include "applib/ui/window_manager.h"
#include "apps/system_apps/timeline/timeline.h"
#include "kernel/pbl_malloc.h"
#include "kernel/pebble_tasks.h"
#include "kernel/ui/kernel_ui.h"
#include "kernel/ui/modals/modal_manager.h"
#include "process_state/app_state/app_state.h"
#include "services/normal/timeline/actions_endpoint.h"
#include "services/normal/timeline/layout_layer.h"
#include "services/normal/timeline/timeline.h"
#include "services/normal/timeline/timeline_actions.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/math.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
///////////////////////////////////////////////////////////
// Drawing functions
///////////////////////////////////////////////////////////
static GSize prv_get_frame_size(TimelineItemLayer *item_layer) {
return item_layer->layer.bounds.size;
}
static int16_t prv_get_height(TimelineItemLayer *item_layer) {
if (item_layer->timeline_layout) {
GSize size = layout_get_size(graphics_context_get_current_context(),
(LayoutLayer *)item_layer->timeline_layout);
return size.h;
} else {
return 0;
}
}
static void prv_update_item(GContext *ctx, TimelineItemLayer *item_layer) {
if (item_layer->timeline_layout) {
GRect bounds = item_layer->timeline_layout->layout_layer.layer.bounds;
bounds.origin.y = 0 - item_layer->scroll_offset_pixels;
layer_set_bounds((Layer *)item_layer->timeline_layout, &bounds);
}
}
///////////////////////////////////////////////////////////
// Scrolling related functions
///////////////////////////////////////////////////////////
static int16_t prv_get_first_scroll_offset(TimelineItemLayer *item_layer) {
if (!item_layer->timeline_layout->has_page_break) {
return 0;
}
return MAX(prv_get_frame_size(item_layer).h, 0);
}
static int16_t prv_get_min_scroll_offset(TimelineItemLayer *item_layer) {
return 0;
}
static int16_t prv_get_max_scroll_offset(TimelineItemLayer *item_layer) {
int16_t max_scroll = prv_get_height(item_layer) - prv_get_frame_size(item_layer).h;
if (max_scroll > 0) {
int16_t first_scroll = prv_get_first_scroll_offset(item_layer);
return MAX(first_scroll, max_scroll);
} else {
return MAX(max_scroll, 0);
}
}
static void prv_scroll_offset_setter(TimelineItemLayer *item_layer, int16_t value) {
item_layer->scroll_offset_pixels = value;
layer_mark_dirty(&item_layer->layer);
}
static int16_t prv_scroll_offset_getter(TimelineItemLayer *item_layer) {
return item_layer->scroll_offset_pixels;
}
static void prv_update_scroll_offset(TimelineItemLayer *item_layer, int16_t new_offset,
bool is_first_scroll) {
static const PropertyAnimationImplementation implementation = {
.base = {
.update = (AnimationUpdateImplementation) property_animation_update_int16,
},
.accessors = {
.setter = { .int16 = (const Int16Setter) prv_scroll_offset_setter, },
.getter = { .int16 = (const Int16Getter) prv_scroll_offset_getter},
},
};
// If we're already at that position, don't bother scheduling an animation
if (item_layer->scroll_offset_pixels == new_offset) {
return;
}
if (item_layer->animation
&& animation_is_scheduled(property_animation_get_animation(item_layer->animation))) {
// Don't do anything if we're already animating to this position from our current position
int16_t offset;
property_animation_get_to_int16(item_layer->animation, &offset);
if (offset == new_offset) {
return;
}
animation_unschedule(property_animation_get_animation(item_layer->animation));
}
if (item_layer->animation) {
property_animation_init(item_layer->animation, &implementation, item_layer, NULL, &new_offset);
} else {
item_layer->animation = property_animation_create(&implementation,
item_layer, NULL, &new_offset);
PBL_ASSERTN(item_layer->animation);
animation_set_auto_destroy(property_animation_get_animation(item_layer->animation), false);
}
Animation *animation = property_animation_get_animation(item_layer->animation);
if (is_first_scroll) {
animation_set_duration(animation, interpolate_moook_duration());
animation_set_custom_interpolation(animation, interpolate_moook);
} else {
animation_set_curve(animation, AnimationCurveEaseOut);
}
animation_schedule(animation);
}
//! Maybe make this part of the style and smaller for smaller text sizes?
static const int SCROLL_AMOUNT = PBL_IF_RECT_ELSE(48, DISP_ROWS - STATUS_BAR_LAYER_HEIGHT);
static const int SCROLL_FUDGE_AMOUNT = PBL_IF_RECT_ELSE(10, 0);
/////////////////////////////////////////
// Click Config
/////////////////////////////////////////
T_STATIC void prv_handle_down_click(ClickRecognizerRef recognizer, void *context) {
TimelineItemLayer *item_layer = (TimelineItemLayer *)context;
int16_t max_scroll = prv_get_max_scroll_offset(item_layer);
const int16_t first_scroll = prv_get_first_scroll_offset(item_layer);
int16_t current_scroll = item_layer->scroll_offset_pixels;
#if PBL_ROUND // align current_scroll with paging for text flow
current_scroll = ROUND_TO_MOD_CEIL(current_scroll, SCROLL_AMOUNT);
#endif
if (max_scroll >= first_scroll && current_scroll < first_scroll) {
prv_update_scroll_offset(item_layer, first_scroll, true);
} else if (current_scroll + SCROLL_AMOUNT + SCROLL_FUDGE_AMOUNT >= max_scroll) {
#if PBL_ROUND
// scroll down to page aligned end of content
max_scroll = ROUND_TO_MOD_CEIL(max_scroll, DISP_ROWS - STATUS_BAR_LAYER_HEIGHT);
#endif
prv_update_scroll_offset(item_layer, max_scroll, false);
} else {
prv_update_scroll_offset(item_layer, current_scroll + SCROLL_AMOUNT, false);
}
layer_mark_dirty(&item_layer->layer);
}
static void prv_handle_select_click(ClickRecognizerRef recognizer, void *context) {
TimelineItemLayer *item_layer = context;
TimelineItemActionGroup *action_group = &item_layer->item->action_group;
const uint8_t num_actions = action_group->num_actions;
ActionMenuLevel *root_level = timeline_actions_create_action_menu_root_level(
num_actions, 0, TimelineItemActionSourceTimeline);
for (int i = 0; i < num_actions; i++) {
timeline_actions_add_action_to_root_level(&action_group->actions[i], root_level);
}
const LayoutColors *colors = layout_get_colors((LayoutLayer *)item_layer->timeline_layout);
ActionMenuConfig config = {
.root_level = root_level,
.context = item_layer->item,
.colors.background = colors->bg_color,
.colors.foreground = colors->primary_color,
};
timeline_actions_push_action_menu(
&config, window_manager_get_window_stack(ModalPriorityNotification));
}
static void prv_handle_up_click(ClickRecognizerRef recognizer, void *context) {
TimelineItemLayer *item_layer = (TimelineItemLayer *)context;
const int16_t min_scroll = prv_get_min_scroll_offset(item_layer);
const int16_t first_scroll = prv_get_first_scroll_offset(item_layer);
int16_t current_scroll = item_layer->scroll_offset_pixels;
#if PBL_ROUND // align current_scroll with paging for text flow
current_scroll = ROUND_TO_MOD_CEIL(current_scroll, SCROLL_AMOUNT);
#endif
if (current_scroll <= first_scroll) {
prv_update_scroll_offset(item_layer, min_scroll, true);
#if PBL_RECT // fudge breaks ROUND display paging
} else if (current_scroll - (SCROLL_AMOUNT + SCROLL_FUDGE_AMOUNT) < first_scroll) {
prv_update_scroll_offset(item_layer, first_scroll, false);
#endif
} else {
prv_update_scroll_offset(item_layer, current_scroll - SCROLL_AMOUNT, false);
}
layer_mark_dirty(&item_layer->layer);
}
static void prv_handle_back_click(ClickRecognizerRef recognizer, void *context) {
timeline_animate_back_from_card();
}
static void timeline_item_layer_click_config_provider(void *context) {
window_single_repeating_click_subscribe(BUTTON_ID_UP, 100, prv_handle_up_click);
window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 100, prv_handle_down_click);
window_single_click_subscribe(BUTTON_ID_SELECT, prv_handle_select_click);
window_set_click_context(BUTTON_ID_UP, context);
window_set_click_context(BUTTON_ID_DOWN, context);
window_set_click_context(BUTTON_ID_SELECT, context);
window_set_click_context(BUTTON_ID_BACK, context);
if (pebble_task_get_current() == PebbleTask_App) {
// only override the back button when we're in the app
window_single_click_subscribe(BUTTON_ID_BACK, prv_handle_back_click);
}
}
void timeline_item_layer_set_click_config_onto_window(TimelineItemLayer *item_layer,
struct Window *window) {
window_set_click_config_provider_with_context(window,
timeline_item_layer_click_config_provider,
item_layer);
}
/////////////////////////////////////////
// Public functions
/////////////////////////////////////////
void timeline_item_layer_update_proc(Layer* layer, GContext* ctx) {
//! Fill background with white to hide layers below
TimelineItemLayer* item_layer = (TimelineItemLayer *)layer;
const LayoutColors *colors = layout_get_colors((LayoutLayer *)item_layer->timeline_layout);
graphics_context_set_fill_color(ctx, colors->bg_color);
graphics_fill_rect(ctx, &layer->bounds);
prv_update_item(ctx, item_layer);
}
void timeline_item_layer_init(TimelineItemLayer *item_layer, const GRect *frame) {
*item_layer = (TimelineItemLayer){};
layer_init(&item_layer->layer, frame);
layer_set_update_proc(&item_layer->layer, timeline_item_layer_update_proc);
layer_set_clips(&item_layer->layer, false);
}
void timeline_item_layer_deinit(TimelineItemLayer *item_layer) {
property_animation_destroy(item_layer->animation);
layer_deinit(&item_layer->layer);
if (item_layer->timeline_layout) {
layout_destroy((LayoutLayer *)item_layer->timeline_layout);
item_layer->timeline_layout = NULL;
}
}
void timeline_item_layer_set_item(TimelineItemLayer *item_layer, TimelineItem *item,
TimelineLayoutInfo *info) {
item_layer->item = item;
if (item_layer->timeline_layout) {
layer_remove_from_parent((Layer *)item_layer->timeline_layout);
layout_destroy((LayoutLayer *)item_layer->timeline_layout);
}
const LayoutLayerConfig config = {
.frame = &(GRect) { GPointZero, item_layer->layer.frame.size },
.attributes = &item_layer->item->attr_list,
.mode = LayoutLayerModeCard,
.app_id = &item->header.parent_id,
.context = info,
};
item_layer->timeline_layout =
(TimelineLayout *)layout_create(item_layer->item->header.layout, &config);
layer_add_child(&item_layer->layer, (Layer *)item_layer->timeline_layout);
}

View file

@ -0,0 +1,67 @@
/*
* 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 <stdbool.h>
#include "applib/graphics/text.h"
#include "applib/ui/layer.h"
#include "applib/ui/property_animation_private.h"
#include "kernel/events.h"
#include "services/normal/timeline/item.h"
#include "services/normal/timeline/timeline_layout.h"
//! The timeline item layer is a mock UI used to display timeline items
//! until actual layouts are implemented. It is somewhat related to the
//! notification layer, although it does not swap between items.
typedef struct {
Layer layer;
//!< The line that's currently at the top of the frame.
int16_t scroll_offset_pixels;
PropertyAnimation *animation;
TimelineItem *item;
TimelineLayout *timeline_layout;
} TimelineItemLayer;
//! The layer update proc for the TimelineItemLayer
void timeline_item_layer_update_proc(Layer* layer, GContext* ctx);
//! Initialize a timeline item layer
//! @param layer a pointer to the TimelineItemLayer to initialize
//! @param frame the frame with which to initialize the layer
void timeline_item_layer_init(TimelineItemLayer *item_layer, const GRect *frame);
//! Deinitialize a timeline item layer. Currently a no-op
void timeline_item_layer_deinit(TimelineItemLayer *item_layer);
//! Set the timeline item displayed by the TimelineItemLayer
//! @param layer a pointer to the TimelineItemLayer
//! @param item a pointer to the item to use
void timeline_item_layer_set_item(TimelineItemLayer *item_layer, TimelineItem *item,
TimelineLayoutInfo *info);
//! Down click handler for the TimelineItemLayer
void timeline_item_layer_down_click_handler(ClickRecognizerRef recognizer, void *context);
//! Up click handler for the TimelineItemLayer
void timeline_item_layer_up_click_handler(ClickRecognizerRef recognizer, void *context);
//! Convenience function to set the \ref ClickConfigProvider callback on the
//! given window to menu layer's internal click config provider. This internal
//! click configuration provider, will set up the default UP & DOWN handlers
void timeline_item_layer_set_click_config_onto_window(TimelineItemLayer *item_layer,
struct Window *window);

109
src/fw/popups/wakeup_ui.c Normal file
View file

@ -0,0 +1,109 @@
/*
* 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 "wakeup_ui.h"
#include "applib/fonts/fonts.h"
#include "applib/ui/dialogs/expandable_dialog.h"
#include "applib/ui/ui.h"
#include "applib/ui/window.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_install_manager.h"
#include "process_state/app_state/app_state.h"
#include "resource/resource_ids.auto.h"
#include "services/common/i18n/i18n.h"
typedef void (*EachCb)(AppInstallEntry *entry, void *data);
void prv_find_len_helper(AppInstallEntry *entry, void *context) {
uint32_t *len = context;
*len += strlen(entry->name) + 1;
}
void prv_string_builder(AppInstallEntry *entry, void *context) {
char *str = context;
strcat(str, entry->name);
strcat(str, "\n");
}
void prv_each_app_ids(uint32_t num_ids, AppInstallId *ids, EachCb cb, void *context) {
for (uint32_t i = 0; i < num_ids; i++) {
// app_title +1 to add newline per line
AppInstallId app_id = ids[i];
AppInstallEntry entry;
if (!app_install_get_entry_for_install_id(app_id, &entry)) {
continue;
}
cb(&entry, context);
}
}
typedef struct {
uint8_t count;
AppInstallId *app_ids;
} WakeupUICbData;
static void prv_show_dialog(void *context) {
WakeupUICbData *data = context;
const char* missed_text_raw =
i18n_noop("While your Pebble was off wakeup events occurred for:\n");
const char* missed_text = i18n_get(missed_text_raw, data);
// Find the size of all of the missed_apps names (no max length defined)
int16_t missed_app_titles_len = 0;
prv_each_app_ids(data->count, data->app_ids, prv_find_len_helper, &missed_app_titles_len);
int16_t missed_message_len = strlen(missed_text) + missed_app_titles_len;
char *missed_message = kernel_zalloc(missed_message_len + 1);
strncpy(missed_message, missed_text, strlen(missed_text));
i18n_free(missed_text, data);
prv_each_app_ids(data->count, data->app_ids, prv_string_builder, missed_message);
missed_message[missed_message_len] = '\0';
// must free the buffer passed in
kernel_free(data->app_ids);
kernel_free(data);
ExpandableDialog * ex_dialog = expandable_dialog_create(NULL);
Dialog *dialog = expandable_dialog_get_dialog(ex_dialog);
dialog_set_text_buffer(dialog, missed_message, true);
dialog_set_icon(dialog, RESOURCE_ID_GENERIC_WARNING_TINY);
dialog_show_status_bar_layer(dialog, true);
expandable_dialog_push(ex_dialog, modal_manager_get_window_stack(ModalPriorityGeneric));
}
// ---------------------------------------------------------------------------
// Display our alert
void wakeup_popup_window(uint8_t missed_apps_count, AppInstallId *missed_app_ids) {
WakeupUICbData *data = kernel_malloc(sizeof(WakeupUICbData));
if (data) {
*data = (WakeupUICbData) {
.count = missed_apps_count,
.app_ids = missed_app_ids,
};
launcher_task_add_callback(prv_show_dialog, data);
}
// don't really care if it fails.
}

29
src/fw/popups/wakeup_ui.h Normal file
View file

@ -0,0 +1,29 @@
/*
* 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 <stdint.h>
#include "process_management/app_install_types.h"
//! This function creates a popup window displaying a missed wakeup events notificatoin
//! along with the application names that were missed
//! Note: missed_apps_ids is free'd by this function
//! @param missed_apps_count number of app names to display
//! @param missed_app_ids an array of AppInstallIds of the app names to display
void wakeup_popup_window(uint8_t missed_apps_count, AppInstallId *missed_app_ids);