mirror of
https://github.com/google/pebble.git
synced 2025-06-09 19:53:12 +00:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
280
src/fw/popups/alarm_popup.c
Normal file
280
src/fw/popups/alarm_popup.c
Normal 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
|
||||
}
|
21
src/fw/popups/alarm_popup.h
Normal file
21
src/fw/popups/alarm_popup.h
Normal 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);
|
74
src/fw/popups/ble_hrm/ble_hrm_reminder_popup.c
Normal file
74
src/fw/popups/ble_hrm/ble_hrm_reminder_popup.c
Normal 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);
|
||||
}
|
19
src/fw/popups/ble_hrm/ble_hrm_reminder_popup.h
Normal file
19
src/fw/popups/ble_hrm/ble_hrm_reminder_popup.h
Normal 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);
|
92
src/fw/popups/ble_hrm/ble_hrm_sharing_popup.c
Normal file
92
src/fw/popups/ble_hrm/ble_hrm_sharing_popup.c
Normal 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);
|
||||
}
|
26
src/fw/popups/ble_hrm/ble_hrm_sharing_popup.h
Normal file
26
src/fw/popups/ble_hrm/ble_hrm_sharing_popup.h
Normal 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
|
37
src/fw/popups/ble_hrm/ble_hrm_stop_sharing_popup.c
Normal file
37
src/fw/popups/ble_hrm/ble_hrm_stop_sharing_popup.c
Normal 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;
|
||||
}
|
21
src/fw/popups/ble_hrm/ble_hrm_stop_sharing_popup.h
Normal file
21
src/fw/popups/ble_hrm/ble_hrm_stop_sharing_popup.h
Normal 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);
|
695
src/fw/popups/bluetooth_pairing_ui.c
Normal file
695
src/fw/popups/bluetooth_pairing_ui.c
Normal 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);
|
||||
}
|
20
src/fw/popups/bluetooth_pairing_ui.h
Normal file
20
src/fw/popups/bluetooth_pairing_ui.h
Normal 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
199
src/fw/popups/crashed_ui.c
Normal 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
|
27
src/fw/popups/crashed_ui.h
Normal file
27
src/fw/popups/crashed_ui.h
Normal 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);
|
124
src/fw/popups/health_tracking_ui.c
Normal file
124
src/fw/popups/health_tracking_ui.c
Normal 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
|
33
src/fw/popups/health_tracking_ui.h
Normal file
33
src/fw/popups/health_tracking_ui.h
Normal 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);
|
1498
src/fw/popups/notifications/notification_window.c
Normal file
1498
src/fw/popups/notifications/notification_window.c
Normal file
File diff suppressed because it is too large
Load diff
50
src/fw/popups/notifications/notification_window.h
Normal file
50
src/fw/popups/notifications/notification_window.h
Normal 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);
|
64
src/fw/popups/notifications/notification_window_private.h
Normal file
64
src/fw/popups/notifications/notification_window_private.h
Normal 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;
|
181
src/fw/popups/notifications/notifications_presented_list.c
Normal file
181
src/fw/popups/notifications/notifications_presented_list.c
Normal 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;
|
||||
}
|
||||
}
|
84
src/fw/popups/notifications/notifications_presented_list.h
Normal file
84
src/fw/popups/notifications/notifications_presented_list.h
Normal 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);
|
106
src/fw/popups/phone_formatting.c
Normal file
106
src/fw/popups/phone_formatting.c
Normal 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);
|
||||
}
|
39
src/fw/popups/phone_formatting.h
Normal file
39
src/fw/popups/phone_formatting.h
Normal 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
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
37
src/fw/popups/phone_ui.h
Normal 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);
|
135
src/fw/popups/switch_worker_ui.c
Normal file
135
src/fw/popups/switch_worker_ui.c
Normal 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);
|
||||
}
|
29
src/fw/popups/switch_worker_ui.h
Normal file
29
src/fw/popups/switch_worker_ui.h
Normal 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);
|
||||
|
575
src/fw/popups/timeline/peek.c
Normal file
575
src/fw/popups/timeline/peek.c
Normal 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
|
120
src/fw/popups/timeline/peek.h
Normal file
120
src/fw/popups/timeline/peek.h
Normal 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);
|
68
src/fw/popups/timeline/peek_animations.c
Normal file
68
src/fw/popups/timeline/peek_animations.c
Normal 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);
|
||||
}
|
27
src/fw/popups/timeline/peek_animations.h
Normal file
27
src/fw/popups/timeline/peek_animations.h
Normal 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);
|
47
src/fw/popups/timeline/peek_private.h
Normal file
47
src/fw/popups/timeline/peek_private.h
Normal 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
|
292
src/fw/popups/timeline/timeline_item_layer.c
Normal file
292
src/fw/popups/timeline/timeline_item_layer.c
Normal 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);
|
||||
}
|
67
src/fw/popups/timeline/timeline_item_layer.h
Normal file
67
src/fw/popups/timeline/timeline_item_layer.h
Normal 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
109
src/fw/popups/wakeup_ui.c
Normal 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
29
src/fw/popups/wakeup_ui.h
Normal 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);
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue