Import of the watch repository from Pebble

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

View file

@ -0,0 +1,173 @@
/*
* 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 "action_chaining_window.h"
#include "applib/ui/ui.h"
#include "kernel/pbl_malloc.h"
typedef struct {
Window window;
MenuLayer menu_layer;
StatusBarLayer status_layer;
const char *title;
TimelineItemActionGroup *action_group;
ActionChainingMenuSelectCb select_cb;
ActionChainingMenuClosedCb closed_cb;
void *select_cb_context;
void *closed_cb_context;
} ChainingWindowData;
#if PBL_ROUND
static int16_t prv_get_header_height(struct MenuLayer *menu_layer,
uint16_t section_index,
void *callback_context) {
return MENU_CELL_ROUND_UNFOCUSED_TALL_CELL_HEIGHT;
}
static void prv_draw_header(GContext *ctx,
const Layer *cell_layer,
uint16_t section_index,
void *callback_context) {
ChainingWindowData *data = callback_context;
const GFont font = fonts_get_system_font(FONT_KEY_GOTHIC_18);
menu_cell_basic_draw_custom(ctx, cell_layer, font, data->title, font, NULL, font,
NULL, NULL, false, GTextOverflowModeWordWrap);
}
#endif
static uint16_t prv_get_num_rows(MenuLayer *menu_layer, uint16_t section_index,
void *callback_context) {
ChainingWindowData *data = callback_context;
return data->action_group->num_actions;
}
static int16_t prv_get_cell_height(struct MenuLayer *menu_layer,
MenuIndex *cell_index,
void *callback_context) {
#if PBL_ROUND
MenuIndex selected_index = menu_layer_get_selected_index(menu_layer);
bool is_selected = menu_index_compare(cell_index, &selected_index) == 0;
return is_selected ? MENU_CELL_ROUND_FOCUSED_SHORT_CELL_HEIGHT :
MENU_CELL_ROUND_UNFOCUSED_TALL_CELL_HEIGHT;
#else
return menu_cell_basic_cell_height();
#endif
}
static void prv_draw_row(GContext *ctx, const Layer *cell_layer,
MenuIndex *cell_index, void *callback_context) {
ChainingWindowData *data = callback_context;
AttributeList *attrs = &data->action_group->actions[cell_index->row].attr_list;
Attribute *title_attr = attribute_find(attrs, AttributeIdTitle);
Attribute *subtitle_attr = attribute_find(attrs, AttributeIdSubtitle);
const char *title = title_attr ? title_attr->cstring : NULL;
const char *subtitle = subtitle_attr ? subtitle_attr->cstring : NULL;
menu_cell_basic_draw(ctx, cell_layer, title, subtitle, NULL);
}
static void prv_select_callback(MenuLayer *menu_layer, MenuIndex *cell_index,
void *callback_context) {
ChainingWindowData *data = callback_context;
if (data->select_cb) {
data->select_cb(&data->window, &data->action_group->actions[cell_index->row],
data->select_cb_context);
}
}
static void prv_chaining_window_unload(Window *window) {
ChainingWindowData *data = window_get_user_data(window);
if (data->closed_cb) {
data->closed_cb(data->closed_cb_context);
}
menu_layer_deinit(&data->menu_layer);
#if PBL_RECT
status_bar_layer_deinit(&data->status_layer);
#endif
kernel_free(data);
}
static void prv_chaining_window_load(Window *window) {
ChainingWindowData *data = window_get_user_data(window);
const GRect bounds = grect_inset(data->window.layer.bounds, (GEdgeInsets) {
.top = STATUS_BAR_LAYER_HEIGHT,
#if PBL_ROUND
.bottom = STATUS_BAR_LAYER_HEIGHT
#endif
});
menu_layer_init(&data->menu_layer, &bounds);
menu_layer_set_callbacks(&data->menu_layer, data, &(MenuLayerCallbacks) {
#if PBL_ROUND
.get_header_height = prv_get_header_height,
.draw_header = prv_draw_header,
#endif
.get_num_rows = prv_get_num_rows,
.get_cell_height = prv_get_cell_height,
.draw_row = prv_draw_row,
.select_click = prv_select_callback,
});
menu_layer_set_highlight_colors(
&data->menu_layer, PBL_IF_COLOR_ELSE(GColorIslamicGreen, GColorBlack), GColorWhite);
menu_layer_set_click_config_onto_window(&data->menu_layer, &data->window);
layer_add_child(&data->window.layer, menu_layer_get_layer(&data->menu_layer));
#if PBL_RECT
status_bar_layer_init(&data->status_layer);
status_bar_layer_set_colors(&data->status_layer, PBL_IF_COLOR_ELSE(GColorWhite, GColorBlack),
PBL_IF_COLOR_ELSE(GColorBlack, GColorWhite));
status_bar_layer_set_title(&data->status_layer, data->title,
false /* revert */, false /* animated */);
status_bar_layer_set_separator_mode(&data->status_layer, StatusBarLayerSeparatorModeDotted);
layer_add_child(&data->window.layer, status_bar_layer_get_layer(&data->status_layer));
#endif
}
void action_chaining_window_push(WindowStack *window_stack, const char *title,
TimelineItemActionGroup *action_group,
ActionChainingMenuSelectCb select_cb,
void *select_cb_context,
ActionChainingMenuClosedCb closed_cb,
void *closed_cb_context) {
ChainingWindowData *data = kernel_zalloc_check(sizeof(ChainingWindowData));
*data = (ChainingWindowData) {
.title = title,
.action_group = action_group,
.select_cb = select_cb,
.select_cb_context = select_cb_context,
.closed_cb = closed_cb,
.closed_cb_context = closed_cb_context,
};
Window *window = &data->window;
window_init(window, WINDOW_NAME("Action Chaining"));
window_set_user_data(window, data);
window_set_window_handlers(window, &(WindowHandlers) {
.load = prv_chaining_window_load,
.unload = prv_chaining_window_unload,
});
window_stack_push(window_stack, window, true);
}

View file

@ -0,0 +1,31 @@
/*
* 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 "services/normal/timeline/item.h"
typedef void (*ActionChainingMenuSelectCb)(Window *chaining_window,
TimelineItemAction *action, void *context);
typedef void (*ActionChainingMenuClosedCb)(void *context);
void action_chaining_window_push(WindowStack *window_stack, const char *title,
TimelineItemActionGroup *action_group,
ActionChainingMenuSelectCb select_cb,
void *select_cb_context,
ActionChainingMenuClosedCb closed_cb,
void *closed_cb_context);

View file

@ -0,0 +1,133 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "alerts.h"
#include "alerts_private.h"
#include "drivers/battery.h"
#include "drivers/rtc.h"
#include "kernel/low_power.h"
#include "services/common/analytics/analytics.h"
#include "services/common/firmware_update.h"
#include "services/normal/notifications/do_not_disturb.h"
#include "services/normal/notifications/alerts_preferences_private.h"
static const int NOTIFICATION_VIBE_HOLDOFF_MS = 3000;
static RtcTicks s_notification_vibe_tick_timestamp = 0;
//////////////////
// Private Functions
//////////////////
static int64_t prv_get_ms_since_last_notification_vibe(void) {
RtcTicks current_ticks = rtc_get_ticks();
int64_t millis_since_last_vibe =
(current_ticks - s_notification_vibe_tick_timestamp) * 1000 / RTC_TICKS_HZ; // x1000 for ms
return millis_since_last_vibe;
}
//////////////////
// Public Functions
//////////////////
void alerts_incoming_alert_analytics() {
if (do_not_disturb_is_active()) {
analytics_inc(ANALYTICS_DEVICE_METRIC_NOTIFICATION_RECEIVED_DND_COUNT, AnalyticsClient_System);
}
}
bool alerts_should_notify_for_type(AlertType type) {
if (low_power_is_active()) {
return false;
}
if (firmware_update_is_in_progress()) {
return false;
}
return alerts_preferences_get_alert_mask() & type;
}
bool alerts_should_enable_backlight_for_type(AlertType type) {
if (do_not_disturb_is_active() && !(alerts_preferences_dnd_get_mask() & type)) {
return false;
}
return alerts_should_notify_for_type(type);
}
bool alerts_should_vibrate_for_type(AlertType type) {
if (do_not_disturb_is_active() && !(alerts_preferences_dnd_get_mask() & type)) {
return false;
}
if (!alerts_should_notify_for_type(type)) {
return false;
}
if (battery_is_usb_connected()) {
return false;
}
if (prv_get_ms_since_last_notification_vibe() < NOTIFICATION_VIBE_HOLDOFF_MS) {
return false;
}
return alerts_preferences_get_vibrate();
}
bool alerts_get_vibrate(void) {
return alerts_preferences_get_vibrate();
}
AlertMask alerts_get_mask(void) {
return alerts_preferences_get_alert_mask();
}
AlertMask alerts_get_dnd_mask(void) {
return alerts_preferences_dnd_get_mask();
}
uint32_t alerts_get_notification_window_timeout_ms(void) {
return alerts_preferences_get_notification_window_timeout_ms();
}
void alerts_set_vibrate(bool enable) {
alerts_preferences_set_vibrate(enable);
}
void alerts_set_mask(AlertMask mask) {
alerts_preferences_set_alert_mask(mask);
}
void alerts_set_dnd_mask(AlertMask mask) {
alerts_preferences_dnd_set_mask(mask);
}
void alerts_set_notification_vibe_timestamp() {
// if we do vibrate, update timestamp of last vibration
s_notification_vibe_tick_timestamp = rtc_get_ticks();
}
void alerts_set_notification_window_timeout_ms(uint32_t timeout_ms) {
alerts_preferences_set_notification_window_timeout_ms(timeout_ms);
}
void alerts_init() {
alerts_preferences_init();
do_not_disturb_init();
vibe_intensity_init();
}

View file

@ -0,0 +1,45 @@
/*
* 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 "services/normal/notifications/notification_types.h"
typedef enum AlertType {
AlertInvalid = NotificationInvalid,
AlertMobile = NotificationMobile,
AlertPhoneCall = NotificationPhoneCall,
AlertOther = NotificationOther,
AlertReminder = NotificationReminder
} AlertType;
// Service to determine how and if the user gets alerted on a call/notification
//! Call this function before alerting the user in any notification/call for the alerts service
//! to handle analytics operations.
void alerts_incoming_alert_analytics();
bool alerts_should_notify_for_type(AlertType type);
bool alerts_should_enable_backlight_for_type(AlertType type);
bool alerts_should_vibrate_for_type(AlertType type);
//! When vibrating for an incoming notification, call this function to prevent multiple vibes
//! within a short period of time.
void alerts_set_notification_vibe_timestamp();

View file

@ -0,0 +1,475 @@
/*
* 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/notifications/alerts_preferences.h"
#include "services/normal/notifications/alerts_preferences_private.h"
#include "drivers/rtc.h"
#include "popups/notifications/notification_window.h"
#include "services/common/analytics/analytics.h"
#include "services/normal/notifications/do_not_disturb.h"
#include "services/normal/settings/settings_file.h"
#include "services/normal/vibes/vibe_intensity.h"
#include "system/passert.h"
#include "os/mutex.h"
#include "util/bitset.h"
#include <string.h>
#define FILE_NAME "notifpref"
#define FILE_LEN (1024)
static PebbleMutex *s_mutex;
///////////////////////////////////
//! Preference keys
///////////////////////////////////
#define PREF_KEY_MASK "mask"
static AlertMask s_mask = AlertMaskAllOn;
#define PREF_KEY_DND_INTERRUPTIONS_MASK "dndInterruptionsMask"
static AlertMask s_dnd_interruptions_mask = AlertMaskAllOff;
#define PREF_KEY_VIBE "vibe"
static bool s_vibe_on_notification = true;
#define PREF_KEY_VIBE_INTENSITY "vibeIntensity"
static VibeIntensity s_vibe_intensity = DEFAULT_VIBE_INTENSITY;
#if CAPABILITY_HAS_VIBE_SCORES
#define PREF_KEY_VIBE_SCORE_NOTIFICATIONS ("vibeScoreNotifications")
static VibeScoreId s_vibe_score_notifications = DEFAULT_VIBE_SCORE_NOTIFS;
#define PREF_KEY_VIBE_SCORE_INCOMING_CALLS ("vibeScoreIncomingCalls")
static VibeScoreId s_vibe_score_incoming_calls = DEFAULT_VIBE_SCORE_INCOMING_CALLS;
#define PREF_KEY_VIBE_SCORE_ALARMS ("vibeScoreAlarms")
static VibeScoreId s_vibe_score_alarms = DEFAULT_VIBE_SCORE_ALARMS;
#endif
#define PREF_KEY_DND_MANUALLY_ENABLED "dndManuallyEnabled"
static bool s_do_not_disturb_manually_enabled = false;
#define PREF_KEY_DND_SMART_ENABLED "dndSmartEnabled"
static bool s_do_not_disturb_smart_dnd_enabled = false;
#define PREF_KEY_FIRST_USE_COMPLETE "firstUseComplete"
static uint32_t s_first_use_complete = 0;
#define PREF_KEY_NOTIF_WINDOW_TIMEOUT "notifWindowTimeout"
static uint32_t s_notif_window_timeout_ms = NOTIF_WINDOW_TIMEOUT_DEFAULT;
///////////////////////////////////
//! Legacy preference keys
///////////////////////////////////
#define PREF_KEY_LEGACY_DND_SCHEDULE "dndSchedule"
static DoNotDisturbSchedule s_legacy_dnd_schedule = {
.from_hour = 0,
.to_hour = 6,
};
#define PREF_KEY_LEGACY_DND_SCHEDULE_ENABLED "dndEnabled"
static bool s_legacy_dnd_schedule_enabled = false;
#define PREF_KEY_LEGACY_DND_MANUAL_FIRST_USE "dndManualFirstUse"
#define PREF_KEY_LEGACY_DND_SMART_FIRST_USE "dndSmartFirstUse"
///////////////////////////////////
//! Variables
///////////////////////////////////
typedef struct DoNotDisturbScheduleConfig {
DoNotDisturbSchedule schedule;
bool enabled;
} DoNotDisturbScheduleConfig;
typedef struct DoNotDisturbScheduleConfigKeys {
const char *schedule_pref_key;
const char *enabled_pref_key;
} DoNotDisturbScheduleConfigKeys;
static DoNotDisturbScheduleConfig s_dnd_schedule[NumDNDSchedules];
static const DoNotDisturbScheduleConfigKeys s_dnd_schedule_keys[NumDNDSchedules] = {
[WeekdaySchedule] = {
.schedule_pref_key = "dndWeekdaySchedule",
.enabled_pref_key = "dndWeekdayScheduleEnabled",
},
[WeekendSchedule] = {
.schedule_pref_key = "dndWeekendSchedule",
.enabled_pref_key = "dndWeekendScheduleEnabled",
}
};
static void prv_migrate_legacy_dnd_schedule(SettingsFile *file) {
// If Weekday schedule does not exist, assume that the other 3 settings files are missing as well
// Set the new schedules to the legacy schedule and delete the legacy schedule
if (!settings_file_exists(file, s_dnd_schedule_keys[WeekdaySchedule].schedule_pref_key,
strlen(s_dnd_schedule_keys[WeekdaySchedule].schedule_pref_key))) {
#define SET_PREF_ALREADY_OPEN(key, value) \
settings_file_set(file, key, strlen(key), value, sizeof(value));
s_dnd_schedule[WeekdaySchedule].schedule = s_legacy_dnd_schedule;
SET_PREF_ALREADY_OPEN(s_dnd_schedule_keys[WeekdaySchedule].schedule_pref_key,
&s_dnd_schedule[WeekdaySchedule].schedule);
s_dnd_schedule[WeekdaySchedule].enabled = s_legacy_dnd_schedule_enabled;
SET_PREF_ALREADY_OPEN(s_dnd_schedule_keys[WeekdaySchedule].enabled_pref_key,
&s_dnd_schedule[WeekdaySchedule].enabled);
s_dnd_schedule[WeekendSchedule].schedule = s_legacy_dnd_schedule;
SET_PREF_ALREADY_OPEN(s_dnd_schedule_keys[WeekendSchedule].schedule_pref_key,
&s_dnd_schedule[WeekendSchedule].schedule);
s_dnd_schedule[WeekendSchedule].enabled = s_legacy_dnd_schedule_enabled;
SET_PREF_ALREADY_OPEN(s_dnd_schedule_keys[WeekendSchedule].enabled_pref_key,
&s_dnd_schedule[WeekendSchedule].enabled);
#undef SET_PREF_ALREADY_OPEN
#define DELETE_PREF(key) \
do { \
if (settings_file_exists(file, key, strlen(key))) { \
settings_file_delete(file, key, strlen(key)); \
} \
} while (0)
DELETE_PREF(PREF_KEY_LEGACY_DND_SCHEDULE);
DELETE_PREF(PREF_KEY_LEGACY_DND_SCHEDULE_ENABLED);
#undef DELETE_PREF
}
}
#if !PLATFORM_TINTIN
static void prv_migrate_legacy_first_use_settings(SettingsFile *file) {
// These don't need to be initialized since settings_file_get will clear them on error
uint8_t manual_dnd_first_use_complete;
bool smart_dnd_first_use_complete;
// Migrate the old first use dialog prefs
#define RESTORE_AND_DELETE_PREF(key, var) \
do { \
if (settings_file_get(file, key, strlen(key), &var, sizeof(var)) == S_SUCCESS) { \
settings_file_delete(file, key, strlen(key)); \
} \
} while (0)
RESTORE_AND_DELETE_PREF(PREF_KEY_LEGACY_DND_MANUAL_FIRST_USE, manual_dnd_first_use_complete);
RESTORE_AND_DELETE_PREF(PREF_KEY_LEGACY_DND_SMART_FIRST_USE, smart_dnd_first_use_complete);
s_first_use_complete |= manual_dnd_first_use_complete << FirstUseSourceManualDNDActionMenu;
s_first_use_complete |= smart_dnd_first_use_complete << FirstUseSourceSmartDND;
#undef RESTORE_AND_DELETE_PREF
}
#endif
#if CAPABILITY_HAS_VIBE_SCORES
static void prv_save_all_vibe_scores_to_file(SettingsFile *file) {
#define SET_PREF_ALREADY_OPEN(key, value) \
settings_file_set(file, key, strlen(key), &value, sizeof(value));
SET_PREF_ALREADY_OPEN(PREF_KEY_VIBE_SCORE_NOTIFICATIONS, s_vibe_score_notifications);
SET_PREF_ALREADY_OPEN(PREF_KEY_VIBE_SCORE_INCOMING_CALLS, s_vibe_score_incoming_calls);
SET_PREF_ALREADY_OPEN(PREF_KEY_VIBE_SCORE_ALARMS, s_vibe_score_alarms);
#undef SET_PREF_ALREADY_OPEN
}
static VibeScoreId prv_return_default_if_invalid(VibeScoreId id, VibeScoreId default_id) {
return vibe_score_info_is_valid(id) ? id : default_id;
}
// Uses the default vibe pattern id if the given score isn't valid
static void prv_ensure_valid_vibe_scores(void) {
s_vibe_score_notifications = prv_return_default_if_invalid(s_vibe_score_notifications,
DEFAULT_VIBE_SCORE_NOTIFS);
s_vibe_score_incoming_calls = prv_return_default_if_invalid(s_vibe_score_incoming_calls,
DEFAULT_VIBE_SCORE_INCOMING_CALLS);
s_vibe_score_alarms = prv_return_default_if_invalid(s_vibe_score_alarms,
DEFAULT_VIBE_SCORE_ALARMS);
}
static void prv_set_vibe_scores_based_on_legacy_intensity(VibeIntensity intensity) {
if (intensity == VibeIntensityHigh) {
s_vibe_score_notifications = VibeScoreId_StandardShortPulseHigh;
s_vibe_score_incoming_calls = VibeScoreId_StandardLongPulseHigh;
s_vibe_score_alarms = VibeScoreId_StandardLongPulseHigh;
} else {
s_vibe_score_notifications = VibeScoreId_StandardShortPulseLow;
s_vibe_score_incoming_calls = VibeScoreId_StandardLongPulseLow;
s_vibe_score_alarms = VibeScoreId_StandardLongPulseLow;
}
}
static void prv_migrate_vibe_intensity_to_vibe_scores(SettingsFile *file) {
// We use the existence of the notifications vibe score pref as a shallow measurement of whether
// or not the user has migrated to vibe scores
const bool user_has_migrated_to_vibe_scores =
settings_file_exists(file, PREF_KEY_VIBE_SCORE_NOTIFICATIONS,
strlen(PREF_KEY_VIBE_SCORE_NOTIFICATIONS));
if (!user_has_migrated_to_vibe_scores) {
// If the user previously set a vibration intensity, set the vibe scores based on that intensity
if (settings_file_exists(file, PREF_KEY_VIBE_INTENSITY, strlen(PREF_KEY_VIBE_INTENSITY))) {
prv_set_vibe_scores_based_on_legacy_intensity(s_vibe_intensity);
} else if (rtc_is_timezone_set()) {
// Otherwise, if the timezone has been set, then we assume this is a user on 3.10 and lower
// that has not touched their vibe intensity preferences.
// rtc_is_timezone_set() was chosen because it is a setting that gets written when the user
// connects their watch to a phone
prv_set_vibe_scores_based_on_legacy_intensity(DEFAULT_VIBE_INTENSITY);
}
}
// PREF_KEY_VIBE, which used to track whether the user enabled/disabled vibrations, has been
// deprecated in favor of the "disabled vibe score", VibeScoreId_Disabled, so switch to using it
// and delete PREF_KEY_VIBE from the settings file if PREF_KEY_VIBE exists in the settings file
if (settings_file_exists(file, PREF_KEY_VIBE, strlen(PREF_KEY_VIBE))) {
if (!s_vibe_on_notification) {
s_vibe_score_notifications = VibeScoreId_Disabled;
s_vibe_score_incoming_calls = VibeScoreId_Disabled;
}
settings_file_delete(file, PREF_KEY_VIBE, strlen(PREF_KEY_VIBE));
}
}
#endif
void alerts_preferences_init(void) {
s_mutex = mutex_create();
SettingsFile file = {{0}};
if (settings_file_open(&file, FILE_NAME, FILE_LEN) != S_SUCCESS) {
return;
}
#define RESTORE_PREF(key, var) \
do { \
__typeof__(var) _tmp; \
if (settings_file_get( \
&file, key, strlen(key), &_tmp, sizeof(_tmp)) == S_SUCCESS) { \
var = _tmp; \
} \
} while (0)
RESTORE_PREF(PREF_KEY_MASK, s_mask);
RESTORE_PREF(PREF_KEY_VIBE, s_vibe_on_notification);
RESTORE_PREF(PREF_KEY_VIBE_INTENSITY, s_vibe_intensity);
#if CAPABILITY_HAS_VIBE_SCORES
RESTORE_PREF(PREF_KEY_VIBE_SCORE_NOTIFICATIONS, s_vibe_score_notifications);
RESTORE_PREF(PREF_KEY_VIBE_SCORE_INCOMING_CALLS, s_vibe_score_incoming_calls);
RESTORE_PREF(PREF_KEY_VIBE_SCORE_ALARMS, s_vibe_score_alarms);
#endif
RESTORE_PREF(PREF_KEY_DND_MANUALLY_ENABLED, s_do_not_disturb_manually_enabled);
RESTORE_PREF(PREF_KEY_DND_SMART_ENABLED, s_do_not_disturb_smart_dnd_enabled);
RESTORE_PREF(PREF_KEY_DND_INTERRUPTIONS_MASK, s_dnd_interruptions_mask);
RESTORE_PREF(PREF_KEY_LEGACY_DND_SCHEDULE, s_legacy_dnd_schedule);
RESTORE_PREF(PREF_KEY_LEGACY_DND_SCHEDULE_ENABLED, s_legacy_dnd_schedule_enabled);
RESTORE_PREF(s_dnd_schedule_keys[WeekdaySchedule].schedule_pref_key,
s_dnd_schedule[WeekdaySchedule].schedule);
RESTORE_PREF(s_dnd_schedule_keys[WeekdaySchedule].enabled_pref_key,
s_dnd_schedule[WeekdaySchedule].enabled);
RESTORE_PREF(s_dnd_schedule_keys[WeekendSchedule].schedule_pref_key,
s_dnd_schedule[WeekendSchedule].schedule);
RESTORE_PREF(s_dnd_schedule_keys[WeekendSchedule].enabled_pref_key,
s_dnd_schedule[WeekendSchedule].enabled);
RESTORE_PREF(PREF_KEY_FIRST_USE_COMPLETE, s_first_use_complete);
RESTORE_PREF(PREF_KEY_NOTIF_WINDOW_TIMEOUT, s_notif_window_timeout_ms);
#undef RESTORE_PREF
prv_migrate_legacy_dnd_schedule(&file);
// tintin watches don't have these prefs, so we can pull this out to save on codespace
#if !PLATFORM_TINTIN
prv_migrate_legacy_first_use_settings(&file);
#endif
#if CAPABILITY_HAS_VIBE_SCORES
prv_migrate_vibe_intensity_to_vibe_scores(&file);
prv_ensure_valid_vibe_scores();
prv_save_all_vibe_scores_to_file(&file);
#endif
settings_file_close(&file);
}
// Convenience macro for setting a string key to a non-pointer value.
#define SET_PREF(key, value) \
prv_set_pref(key, strlen(key), &value, sizeof(value))
static void prv_set_pref(const void *key, size_t key_len, const void *value,
size_t value_len) {
mutex_lock(s_mutex);
SettingsFile file = {{0}};
if (settings_file_open(&file, FILE_NAME, FILE_LEN) != S_SUCCESS) {
goto cleanup;
}
settings_file_set(&file, key, key_len, value, value_len);
settings_file_close(&file);
cleanup:
mutex_unlock(s_mutex);
}
AlertMask alerts_preferences_get_alert_mask(void) {
if (s_mask == AlertMaskAllOnLegacy) {
// Migration for notification settings previously configured under
// old bit setup.
alerts_preferences_set_alert_mask(AlertMaskAllOn);
}
return s_mask;
}
void alerts_preferences_set_alert_mask(AlertMask mask) {
s_mask = mask;
SET_PREF(PREF_KEY_MASK, s_mask);
}
uint32_t alerts_preferences_get_notification_window_timeout_ms(void) {
return s_notif_window_timeout_ms;
}
void alerts_preferences_set_notification_window_timeout_ms(uint32_t timeout_ms) {
s_notif_window_timeout_ms = timeout_ms;
SET_PREF(PREF_KEY_NOTIF_WINDOW_TIMEOUT, s_notif_window_timeout_ms);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
//! Vibes
bool alerts_preferences_get_vibrate(void) {
return s_vibe_on_notification;
}
void alerts_preferences_set_vibrate(bool enable) {
s_vibe_on_notification = enable;
SET_PREF(PREF_KEY_VIBE, s_vibe_on_notification);
}
VibeIntensity alerts_preferences_get_vibe_intensity(void) {
return s_vibe_intensity;
}
void alerts_preferences_set_vibe_intensity(VibeIntensity intensity) {
s_vibe_intensity = intensity;
SET_PREF(PREF_KEY_VIBE_INTENSITY, s_vibe_intensity);
}
#if CAPABILITY_HAS_VIBE_SCORES
VibeScoreId alerts_preferences_get_vibe_score_for_client(VibeClient client) {
switch (client) {
case VibeClient_Notifications:
return s_vibe_score_notifications;
case VibeClient_PhoneCalls:
return s_vibe_score_incoming_calls;
case VibeClient_Alarms:
return s_vibe_score_alarms;
default:
WTF;
}
}
void alerts_preferences_set_vibe_score_for_client(VibeClient client, VibeScoreId id) {
const char *key = NULL;
switch (client) {
case VibeClient_Notifications: {
s_vibe_score_notifications = id;
key = PREF_KEY_VIBE_SCORE_NOTIFICATIONS;
break;
}
case VibeClient_PhoneCalls: {
s_vibe_score_incoming_calls = id;
key = PREF_KEY_VIBE_SCORE_INCOMING_CALLS;
break;
}
case VibeClient_Alarms: {
s_vibe_score_alarms = id;
key = PREF_KEY_VIBE_SCORE_ALARMS;
break;
}
default: {
WTF;
}
}
SET_PREF(key, id);
}
#endif
///////////////////////////////////////////////////////////////////////////////////////////////////
//! DND
void alerts_preferences_dnd_set_mask(AlertMask mask) {
s_dnd_interruptions_mask = mask;
SET_PREF(PREF_KEY_DND_INTERRUPTIONS_MASK, s_dnd_interruptions_mask);
}
AlertMask alerts_preferences_dnd_get_mask(void) {
return s_dnd_interruptions_mask;
}
bool alerts_preferences_dnd_is_manually_enabled(void) {
return s_do_not_disturb_manually_enabled;
}
void alerts_preferences_dnd_set_manually_enabled(bool enable) {
s_do_not_disturb_manually_enabled = enable;
SET_PREF(PREF_KEY_DND_MANUALLY_ENABLED, s_do_not_disturb_manually_enabled);
}
void alerts_preferences_dnd_get_schedule(DoNotDisturbScheduleType type,
DoNotDisturbSchedule *schedule_out) {
*schedule_out = s_dnd_schedule[type].schedule;
};
void alerts_preferences_dnd_set_schedule(DoNotDisturbScheduleType type,
const DoNotDisturbSchedule *schedule) {
s_dnd_schedule[type].schedule = *schedule;
SET_PREF(s_dnd_schedule_keys[type].schedule_pref_key, s_dnd_schedule[type].schedule);
};
bool alerts_preferences_dnd_is_schedule_enabled(DoNotDisturbScheduleType type) {
return s_dnd_schedule[type].enabled;
}
void alerts_preferences_dnd_set_schedule_enabled(DoNotDisturbScheduleType type, bool on) {
s_dnd_schedule[type].enabled = on;
SET_PREF(s_dnd_schedule_keys[type].enabled_pref_key, s_dnd_schedule[type].enabled);
}
bool alerts_preferences_check_and_set_first_use_complete(FirstUseSource source) {
if (s_first_use_complete & (1 << source)) {
return true;
};
s_first_use_complete |= (1 << source);
SET_PREF(PREF_KEY_FIRST_USE_COMPLETE, s_first_use_complete);
return false;
}
bool alerts_preferences_dnd_is_smart_enabled(void) {
return s_do_not_disturb_smart_dnd_enabled;
}
void alerts_preferences_dnd_set_smart_enabled(bool enable) {
s_do_not_disturb_smart_dnd_enabled = enable;
SET_PREF(PREF_KEY_DND_SMART_ENABLED, s_do_not_disturb_smart_dnd_enabled);
}
void analytics_external_collect_alerts_preferences(void) {
uint8_t alerts_dnd_prefs_bitmask = 0;
alerts_dnd_prefs_bitmask |= (alerts_preferences_dnd_is_manually_enabled() << 0);
alerts_dnd_prefs_bitmask |= (alerts_preferences_dnd_is_smart_enabled() << 1);
alerts_dnd_prefs_bitmask |= (alerts_preferences_dnd_is_schedule_enabled(WeekdaySchedule) << 2);
alerts_dnd_prefs_bitmask |= (alerts_preferences_dnd_is_schedule_enabled(WeekendSchedule) << 3);
analytics_set(ANALYTICS_DEVICE_METRIC_ALERTS_DND_PREFS_BITMASK,
alerts_dnd_prefs_bitmask, AnalyticsClient_System);
analytics_set(ANALYTICS_DEVICE_METRIC_ALERTS_MASK,
(uint8_t) alerts_preferences_get_alert_mask(), AnalyticsClient_System);
}

View file

@ -0,0 +1,38 @@
/*
* 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>
typedef enum FirstUseSource {
FirstUseSourceManualDNDActionMenu = 0,
FirstUseSourceManualDNDSettingsMenu,
FirstUseSourceSmartDND,
FirstUseSourceDismiss
} FirstUseSource;
typedef enum MuteBitfield {
MuteBitfield_None = 0b00000000,
MuteBitfield_Always = 0b01111111,
MuteBitfield_Weekdays = 0b00111110,
MuteBitfield_Weekends = 0b01000001,
} MuteBitfield;
//! Checks whether a given "first use" dialog has been shown and sets it as complete
//! @param source The "first use" bit to check
//! @return true if the dialog has already been shown, false otherwise
bool alerts_preferences_check_and_set_first_use_complete(FirstUseSource source);

View file

@ -0,0 +1,78 @@
/*
* 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 "services/normal/notifications/alerts_private.h"
#include "services/normal/notifications/do_not_disturb.h"
#include "services/normal/notifications/notification_types.h"
#include "services/normal/vibes/vibe_intensity.h"
#include "util/time/time.h"
#if CAPABILITY_HAS_VIBE_SCORES
#include "services/normal/vibes/vibe_client.h"
#include "services/normal/vibes/vibe_score_info.h"
#endif
#define NOTIF_WINDOW_TIMEOUT_INFINITE ((uint32_t)~0)
#define NOTIF_WINDOW_TIMEOUT_DEFAULT (3 * MS_PER_MINUTE)
void alerts_preferences_init(void);
AlertMask alerts_preferences_get_alert_mask(void);
void alerts_preferences_set_alert_mask(AlertMask mask);
AlertMask alerts_preferences_dnd_get_mask(void);
void alerts_preferences_dnd_set_mask(AlertMask mask);
uint32_t alerts_preferences_get_notification_window_timeout_ms(void);
void alerts_preferences_set_notification_window_timeout_ms(uint32_t timeout_ms);
bool alerts_preferences_get_vibrate(void);
void alerts_preferences_set_vibrate(bool enable);
VibeIntensity alerts_preferences_get_vibe_intensity(void);
void alerts_preferences_set_vibe_intensity(VibeIntensity intensity);
#if CAPABILITY_HAS_VIBE_SCORES
VibeScoreId alerts_preferences_get_vibe_score_for_client(VibeClient client);
void alerts_preferences_set_vibe_score_for_client(VibeClient client, VibeScoreId id);
#endif
bool alerts_preferences_dnd_is_manually_enabled(void);
void alerts_preferences_dnd_set_manually_enabled(bool enable);
void alerts_preferences_dnd_get_schedule(DoNotDisturbScheduleType type,
DoNotDisturbSchedule *schedule_out);
void alerts_preferences_dnd_set_schedule(DoNotDisturbScheduleType type,
const DoNotDisturbSchedule *schedule);
bool alerts_preferences_dnd_is_schedule_enabled(DoNotDisturbScheduleType type);
void alerts_preferences_dnd_set_schedule_enabled(DoNotDisturbScheduleType type, bool enable);
bool alerts_preferences_dnd_is_smart_enabled(void);
void alerts_preferences_dnd_set_smart_enabled(bool enable);

View file

@ -0,0 +1,47 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "services/normal/notifications/alerts.h"
typedef enum AlertMask {
AlertMaskAllOff = 0,
AlertMaskPhoneCalls = NotificationPhoneCall,
AlertMaskOther = NotificationOther,
AlertMaskAllOnLegacy =
NotificationMobile | NotificationPhoneCall | NotificationOther,
AlertMaskAllOn =
NotificationMobile | NotificationPhoneCall | NotificationOther | NotificationReminder
} AlertMask;
bool alerts_get_vibrate(void);
AlertMask alerts_get_mask(void);
AlertMask alerts_get_dnd_mask(void);
uint32_t alerts_get_notification_window_timeout_ms(void);
void alerts_set_vibrate(bool enable);
void alerts_set_mask(AlertMask mask);
void alerts_set_dnd_mask(AlertMask mask);
void alerts_set_notification_window_timeout_ms(uint32_t timeout_ms);
void alerts_init(void);

View file

@ -0,0 +1,145 @@
/*
* 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 "ancs_filtering.h"
#include "drivers/rtc.h"
#include "kernel/pbl_malloc.h"
#include "services/normal/notifications/alerts_preferences.h"
#include "services/normal/timeline/attributes_actions.h"
#include "system/logging.h"
#include "util/pstring.h"
void ancs_filtering_record_app(iOSNotifPrefs **notif_prefs,
const ANCSAttribute *app_id,
const ANCSAttribute *display_name,
const ANCSAttribute *title) {
// When we receive a notification, information about the app that sent us the notification
// is recorded in the notif_pref_db. We sync this DB with the phone which allows us to
// do things like add non ANCS actions, or filter notifications by app
// The "default" attributes are merged with any existing attributes. This makes it easy to add
// new attributes in the future as well as support EMail / SMS apps which already have data
// stored.
iOSNotifPrefs *app_notif_prefs = *notif_prefs;
const int num_existing_attribtues = app_notif_prefs ? app_notif_prefs->attr_list.num_attributes :
0;
AttributeList new_attr_list;
attribute_list_init_list(num_existing_attribtues, &new_attr_list);
bool list_dirty = false;
// Copy over all the existing attributes to our new list
if (app_notif_prefs) {
for (int i = 0; i < num_existing_attribtues; i++) {
new_attr_list.attributes[i] = app_notif_prefs->attr_list.attributes[i];
}
}
// The app name should be the display name
// If there is no display name (Apple Pay) then fallback to the title
const ANCSAttribute *app_name_attr = NULL;
if (display_name && display_name->length > 0) {
app_name_attr = display_name;
} else if (title && title->length > 0) {
app_name_attr = title;
}
char *app_name_buff = NULL;
if (app_name_attr) {
const char *existing_name = "";
if (app_notif_prefs) {
existing_name = attribute_get_string(&app_notif_prefs->attr_list, AttributeIdAppName, "");
}
if (!pstring_equal_cstring(&app_name_attr->pstr, existing_name)) {
// If the existing name doesn't match our new name, update the name
app_name_buff = kernel_zalloc_check(app_name_attr->length + 1);
pstring_pstring16_to_string(&app_name_attr->pstr, app_name_buff);
attribute_list_add_cstring(&new_attr_list, AttributeIdAppName, app_name_buff);
list_dirty = true;
PBL_LOG(LOG_LEVEL_INFO, "Adding app name to app prefs: <%s>", app_name_buff);
}
}
// Add the mute attribute if we don't have one already
// Default the app to not muted
const bool already_has_mute =
app_notif_prefs && attribute_find(&app_notif_prefs->attr_list, AttributeIdMuteDayOfWeek);
if (!already_has_mute) {
attribute_list_add_uint8(&new_attr_list, AttributeIdMuteDayOfWeek, MuteBitfield_None);
list_dirty = true;
}
// Add / update the "last seen" timestamp
Attribute *last_updated = NULL;
if (app_notif_prefs) {
last_updated = attribute_find(&app_notif_prefs->attr_list, AttributeIdLastUpdated);
}
uint32_t now = rtc_get_time();
// Only perform an update if there is no timestamp or the current timestamp is more than a day old
if (!last_updated ||
(last_updated && now > (last_updated->uint32 + SECONDS_PER_DAY))) {
attribute_list_add_uint32(&new_attr_list, AttributeIdLastUpdated, now);
list_dirty = true;
PBL_LOG(LOG_LEVEL_INFO, "Updating / adding timestamp to app prefs");
}
if (list_dirty) {
// We don't change or add actions at this time
TimelineItemActionGroup *new_action_group = NULL;
if (app_notif_prefs) {
new_action_group = &app_notif_prefs->action_group;
}
ios_notif_pref_db_store_prefs(app_id->value, app_id->length,
&new_attr_list, new_action_group);
// Update our copy of the prefs with the new data
const size_t buf_size = attributes_actions_get_buffer_size(&new_attr_list, new_action_group);
*notif_prefs = kernel_zalloc_check(sizeof(iOSNotifPrefs) + buf_size);
uint8_t *buffer = (uint8_t*)*notif_prefs + sizeof(iOSNotifPrefs);
attributes_actions_deep_copy(&new_attr_list, &(*notif_prefs)->attr_list, new_action_group,
&(*notif_prefs)->action_group, buffer, buffer + buf_size);
ios_notif_pref_db_free_prefs(app_notif_prefs);
}
kernel_free(app_name_buff);
attribute_list_destroy_list(&new_attr_list);
}
uint8_t ancs_filtering_get_mute_type(const iOSNotifPrefs *app_notif_prefs) {
if (app_notif_prefs) {
return attribute_get_uint8(&app_notif_prefs->attr_list,
AttributeIdMuteDayOfWeek,
MuteBitfield_None);
}
return MuteBitfield_None;
}
bool ancs_filtering_is_muted(const iOSNotifPrefs *app_notif_prefs) {
uint8_t mute_type = ancs_filtering_get_mute_type(app_notif_prefs);
struct tm now_tm;
time_t now = rtc_get_time();
localtime_r(&now, &now_tm);
return mute_type & (1 << now_tm.tm_wday);
}

View file

@ -0,0 +1,42 @@
/*
* 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 "comm/ble/kernel_le_client/ancs/ancs_types.h"
#include "services/normal/blob_db/ios_notif_pref_db.h"
//! Updates the entry in the notif pref db for a given app
//! @param app_notif_prefs The existing prefs for the app we want to update. These prefs could be
//! updated in the process
//! @param app_id ID of the app we're recording
//! @param display_name Display name of the app we're recording
//! @param title Title attribute from the notification associated with the app we're recording
//! (fallback in the event that we don't have a display name such as in the case of Apple Pay)
void ancs_filtering_record_app(iOSNotifPrefs **app_notif_prefs,
const ANCSAttribute *app_id,
const ANCSAttribute *display_name,
const ANCSAttribute *title);
//! Returns true if a given app is muted for the current day
//! @param app_notif_prefs Prefs for the given app loaded from the notif pref db
//! @return true if the given app is muted
bool ancs_filtering_is_muted(const iOSNotifPrefs *app_notif_prefs);
//! Returns the mute type for an app
//! @param app_notif_prefs Prefs for the given app loaded from the notif pref db
//! @return MuteBitfield which is the mute type of the app
uint8_t ancs_filtering_get_mute_type(const iOSNotifPrefs *app_notif_prefs);

View file

@ -0,0 +1,485 @@
/*
* 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 "ancs_item.h"
#include "ancs_notifications_util.h"
#include "applib/graphics/utf8.h"
#include "kernel/pbl_malloc.h"
#include "services/common/i18n/i18n.h"
#include "system/logging.h"
#include "util/string.h"
#include <stdio.h>
//! Fits the maximum string "sent an attachment" and i18n translations,
//! plus the emoji, newline and quotes when there is a text message in addition to media.
#define MULTIMEDIA_INDICATOR_LENGTH 64
#define MULTIMEDIA_EMOJI "🎁"
// AttributeIdTitle + AttributeIdAncsAction. See prv_fill_native_ancs_action
static const int NUM_NATIVE_ANCS_ACTION_ATTRS = 2;
static const char s_utf8_ellipsis[] = UTF8_ELLIPSIS_STRING;
static int prv_add_ellipsis(char *buffer, int length) {
// Note: s_utf8_ellipsis is null terminated
memcpy(&buffer[length], s_utf8_ellipsis, sizeof(s_utf8_ellipsis));
return strlen(s_utf8_ellipsis);
}
static bool prv_should_add_sender_attr(const ANCSAttribute *app_id, const ANCSAttribute *title) {
return ((ancs_notifications_util_is_sms(app_id) || ancs_notifications_util_is_phone(app_id)) &&
title && title->length > 0);
}
//! @param buffer The buffer into which to copy the pstring attr. The buffer
//! is assumed to be large enough to contain the string plus an optional
//! ellipsis plus the zero terminator.
//! @param add_ellipsis True if ellipsis must be added to buffer
static char *prv_copy_pstring_and_add_ellipsis(const PascalString16 *pstring, char *buffer,
bool add_ellipsis) {
size_t bytes_added = pstring->str_length + 1;
pstring_pstring16_to_string(pstring, buffer);
if (add_ellipsis) {
bytes_added += prv_add_ellipsis(buffer, pstring->str_length);
}
return buffer + bytes_added;
}
static size_t prv_max_ellipsified_cstring_size(const ANCSAttribute *attr) {
if ((attr == NULL) || (attr->length == 0)) {
return 0;
}
return (size_t) attr->length + strlen(s_utf8_ellipsis) + 1 /* zero terminator */;
}
static uint8_t *prv_add_pstring_to_attribute(uint8_t *buffer, const ANCSAttribute *ancs_attr,
int max_length, Attribute *attribute,
AttributeId attribute_id) {
attribute_init_string(attribute, (char *)buffer, attribute_id);
return (uint8_t *)prv_copy_pstring_and_add_ellipsis(&ancs_attr->pstr, (char *)buffer,
(ancs_attr->length == max_length));
}
static uint8_t *prv_add_action_msg_to_attribute(
uint8_t *buffer, const ANCSAttribute *sender, int sender_max_length,
const ANCSAttribute *caption, int caption_max_length, const char *action_msg,
Attribute *attribute, AttributeId attribute_id) {
// Sets an attribute to <sender> ' ' <action_msg> ( '\n' '"' <caption> '"' )
// For example, sender="Huy Tran", action_msg="sent an attachment", caption="Check this out!"
// The attribute becomes 'Huy Tran sent an attachment\n"Check this out!"'
attribute_init_string(attribute, (char *)buffer, attribute_id);
const char *stripped_caption = NULL;
char caption_buf[caption ? caption->length + 1 : 0];
if (caption && caption->length > 0) {
pstring_pstring16_to_string(&caption->pstr, caption_buf);
// Inserting a caption to an image can easily cause accidental leading whitespace
stripped_caption = string_strip_leading_whitespace(caption_buf);
}
const size_t max_msg_length =
sender->length + (stripped_caption ? strlen(stripped_caption) : 0) +
MULTIMEDIA_INDICATOR_LENGTH + strlen(s_utf8_ellipsis) + 1;
// Sender and action message
int pos = snprintf((char *)buffer, max_msg_length, "%.*s %s",
sender->length, (char *)sender->value, action_msg);
if (pos < (int)max_msg_length && stripped_caption && !IS_EMPTY_STRING(stripped_caption)) {
// Quoted caption
pos += snprintf((char *)buffer + pos, max_msg_length - pos, "\n\"%s\"",
stripped_caption);
if (caption->length == caption_max_length) {
// Overwrite the last quote with ellipsis
const size_t quote_len = 1;
pos += prv_add_ellipsis((char *)buffer, pos - quote_len);
}
}
return buffer + pos + 1;
}
static int prv_set_multimedia_action_msg(char *buffer, size_t length) {
#if PLATFORM_TINTIN
const char *emoji_str = "";
#else
const char *emoji_str = PBL_IF_RECT_ELSE(" " MULTIMEDIA_EMOJI, "\n" MULTIMEDIA_EMOJI);
#endif
return snprintf(buffer, length, "%s%s", i18n_get("sent an attachment", __FILE__),
emoji_str);
}
//! @param buffer Pointer to a buffer large enough to hold all attribute strings required by
//! the action. If buffer points to null, a new buffer will be allocated
//! @return Pointer to the end of the buffer (*buffer + size of strings)
static uint8_t *prv_fill_native_ancs_action(uint8_t **buffer,
TimelineItemAction *action,
ActionId ancs_action_id,
const ANCSAttribute *title,
const ANCSAttribute *app_id,
ANCSProperty properties) {
const bool is_phone_app = ancs_notifications_util_is_phone(app_id);
const bool is_voice_mail = (is_phone_app && (properties & ANCSProperty_VoiceMail));
if (ancs_action_id == ActionIDNegative) {
action->type = is_voice_mail ? TimelineItemActionTypeAncsDelete :
TimelineItemActionTypeAncsNegative;
} else {
action->type = is_phone_app ? TimelineItemActionTypeAncsDial :
TimelineItemActionTypeAncsPositive;
}
action->attr_list.attributes[0].id = AttributeIdAncsAction;
action->attr_list.attributes[0].uint8 = ancs_action_id;
// Allocate a new buffer if none provided
if (!(*buffer)) {
*buffer = task_malloc_check(prv_max_ellipsified_cstring_size(title));
}
uint8_t *rv = prv_add_pstring_to_attribute(*buffer, title, ACTION_MAX_LENGTH,
&action->attr_list.attributes[1], AttributeIdTitle);
// We want to rename the "Clear" action to "Dismiss"
if (strcmp(action->attr_list.attributes[1].cstring, "Clear") == 0) {
// TODO: PBL-23915
// We leak this i18n'd string because not leaking it is really hard.
// We make sure we only ever allocate it once though, so it's not the end of the world.
action->attr_list.attributes[1].cstring = (char*)i18n_get("Dismiss", __FILE__);
}
// We want to rename the "Dial" action to "Call Back"
if (strcmp(action->attr_list.attributes[1].cstring, "Dial") == 0) {
// TODO: PBL-23915
// We leak this i18n'd string because not leaking it is really hard.
// We make sure we only ever allocate it once though, so it's not the end of the world.
action->attr_list.attributes[1].cstring = (char*)i18n_get("Call Back", __FILE__);
}
return rv;
}
static uint8_t *prv_fill_pebble_ancs_action(uint8_t **buffer,
uint8_t *buf_end,
TimelineItemAction *action,
TimelineItemAction *pbl_action) {
action->type = pbl_action->type;
action->id = pbl_action->id;
Attribute *cur_attribute = &action->attr_list.attributes[0];
for (int i = 0; i < pbl_action->attr_list.num_attributes; i++) {
attribute_copy(cur_attribute, &pbl_action->attr_list.attributes[i], buffer, buf_end);
cur_attribute++;
}
return *buffer;
}
static void prv_populate_attributes(TimelineItem *item,
uint8_t **buffer,
const ANCSAttribute *title,
const ANCSAttribute *display_name,
const ANCSAttribute *subtitle,
const ANCSAttribute *message,
const ANCSAttribute *app_id,
const ANCSAppMetadata *app_metadata,
bool has_multimedia) {
int attr_idx = 0;
if (prv_should_add_sender_attr(app_id, title)) {
// Copy the title into the sender attribute so we don't lose it in the multimedia case
*buffer = prv_add_pstring_to_attribute(*buffer, title, TITLE_MAX_LENGTH,
&item->attr_list.attributes[attr_idx],
AttributeIdSender);
attr_idx++;
}
// The sender is in the title for iMessage
const ANCSAttribute *sender = title;
if (has_multimedia) {
// Move the title (sender) to the body for MMS
if (ancs_notifications_util_is_group_sms(app_id, subtitle)) {
// Promote the subtitle (group name) for Group MMS
title = subtitle;
subtitle = NULL;
} else {
title = NULL;
}
}
if (title && title->length > 0) {
*buffer = prv_add_pstring_to_attribute(*buffer, title, TITLE_MAX_LENGTH,
&item->attr_list.attributes[attr_idx],
AttributeIdTitle);
attr_idx++;
}
if (display_name && display_name->length > 0) {
*buffer = prv_add_pstring_to_attribute(*buffer, display_name, TITLE_MAX_LENGTH,
&item->attr_list.attributes[attr_idx],
AttributeIdAppName);
attr_idx++;
}
if (subtitle && subtitle->length > 0) {
*buffer = prv_add_pstring_to_attribute(*buffer, subtitle, SUBTITLE_MAX_LENGTH,
&item->attr_list.attributes[attr_idx],
AttributeIdSubtitle);
attr_idx++;
}
if (sender && sender->length > 0 && has_multimedia) {
char action_msg[MULTIMEDIA_INDICATOR_LENGTH];
prv_set_multimedia_action_msg(action_msg, sizeof(action_msg));
*buffer = prv_add_action_msg_to_attribute(*buffer, sender, TITLE_MAX_LENGTH,
message, MESSAGE_MAX_LENGTH,
action_msg, &item->attr_list.attributes[attr_idx],
AttributeIdBody);
attr_idx++;
} else if (message && message->length > 0) {
*buffer = prv_add_pstring_to_attribute(*buffer, message, MESSAGE_MAX_LENGTH,
&item->attr_list.attributes[attr_idx],
AttributeIdBody);
attr_idx++;
}
if (app_id && app_id->length > 0) {
*buffer = prv_add_pstring_to_attribute(*buffer, app_id, APP_ID_MAX_LENGTH,
&item->attr_list.attributes[attr_idx],
AttributeIdiOSAppIdentifier);
attr_idx++;
}
// add the icon attribute
item->attr_list.attributes[attr_idx].id = AttributeIdIconTiny;
item->attr_list.attributes[attr_idx].uint32 = app_metadata->icon_id;
attr_idx++;
#if PBL_COLOR
if (app_metadata->app_color != 0) {
item->attr_list.attributes[attr_idx].id = AttributeIdBgColor;
item->attr_list.attributes[attr_idx].uint8 = app_metadata->app_color;
}
#endif
}
static bool prv_should_hide_reply_because_group_sms(const TimelineItemAction *action,
const ANCSAttribute *app_id,
const ANCSAttribute *subtitle) {
if (action->type != TimelineItemActionTypeAncsResponse) {
return false;
}
return ancs_notifications_util_is_group_sms(app_id, subtitle);
}
static void prv_populate_actions(TimelineItem *item,
uint8_t **buffer,
uint8_t *buf_end,
const ANCSAttribute *positive_action,
const ANCSAttribute *negative_action,
const ANCSAttribute *subtitle,
const ANCSAttribute *app_id,
const TimelineItemActionGroup *pebble_actions,
ANCSProperty properties) {
TimelineItemAction *action = item->action_group.actions;
// The order the actions get filled is important. See comment in ancs_item_create_and_populate
if (positive_action) {
*buffer = prv_fill_native_ancs_action(buffer, action, ActionIDPositive,
positive_action, app_id, properties);
action++;
}
if (negative_action) {
*buffer = prv_fill_native_ancs_action(buffer, action, ActionIDNegative,
negative_action, app_id, properties);
action++;
}
if (pebble_actions) {
for (int i = 0; i < pebble_actions->num_actions; i++) {
TimelineItemAction *pbl_action = &pebble_actions->actions[i];
if (prv_should_hide_reply_because_group_sms(pbl_action, app_id, subtitle)) {
continue;
}
*buffer = prv_fill_pebble_ancs_action(buffer, buf_end, action, pbl_action);
action++;
}
}
}
TimelineItem *ancs_item_create_and_populate(ANCSAttribute *notif_attributes[],
ANCSAttribute *app_attributes[],
const ANCSAppMetadata *app_metadata,
iOSNotifPrefs *notif_prefs,
time_t timestamp,
ANCSProperty properties) {
const ANCSAttribute *app_id = notif_attributes[FetchedNotifAttributeIndexAppID];
const ANCSAttribute *display_name = app_attributes[FetchedAppAttributeIndexDisplayName];
const ANCSAttribute *title = notif_attributes[FetchedNotifAttributeIndexTitle];
const bool has_multimedia = (ancs_notifications_util_is_sms(app_id) &&
(properties & ANCSProperty_MultiMedia));
if (display_name) {
// dedupe title & display name, they often are the same
if (pstring_equal(&display_name->pstr, &title->pstr)) {
title = NULL;
}
// Hide display name if we have custom app metadata for this app.
// If the app_metadata, not not have a name, then we have the generic app metadata.
if (app_metadata->app_id) {
display_name = NULL;
}
}
const ANCSAttribute *subtitle = notif_attributes[FetchedNotifAttributeIndexSubtitle];
const ANCSAttribute *message = notif_attributes[FetchedNotifAttributeIndexMessage];
// Action labels (optional)
const ANCSAttribute *positive_action =
notif_attributes[FetchedNotifAttributeIndexPositiveActionLabel];
if (positive_action && positive_action->length == 0) {
positive_action = NULL;
}
const ANCSAttribute *negative_action =
notif_attributes[FetchedNotifAttributeIndexNegativeActionLabel];
if (negative_action && negative_action->length == 0) {
negative_action = NULL;
}
// See if we support any additional actions (beyond what ANCS supports) for this type of notif
if (app_id && app_id->length == 0) {
app_id = NULL;
}
// At this point we know that the attributes we have extracted are valid and the sizes thereof
// can be trusted (no error from ancs_util_attr_ptrs). If the length of any of the strings is the
// max allowed by ANCS, assume that the message has been truncated and add an ellipsis to the
// string
size_t required_space_for_strings = 0;
required_space_for_strings += prv_max_ellipsified_cstring_size(title);
required_space_for_strings += prv_max_ellipsified_cstring_size(display_name);
required_space_for_strings += prv_max_ellipsified_cstring_size(subtitle);
if (has_multimedia) {
required_space_for_strings += MULTIMEDIA_INDICATOR_LENGTH;
}
required_space_for_strings += prv_max_ellipsified_cstring_size(message);
required_space_for_strings += prv_max_ellipsified_cstring_size(positive_action);
required_space_for_strings += prv_max_ellipsified_cstring_size(negative_action);
required_space_for_strings += prv_max_ellipsified_cstring_size(app_id);
if (prv_should_add_sender_attr(app_id, title)) {
required_space_for_strings += prv_max_ellipsified_cstring_size(title);
}
int num_pebble_actions = 0;
if (notif_prefs) {
for (int i = 0; i < notif_prefs->action_group.num_actions; i++) {
TimelineItemAction *action = &notif_prefs->action_group.actions[i];
if (prv_should_hide_reply_because_group_sms(action, app_id, subtitle)) {
continue;
}
required_space_for_strings += attribute_list_get_buffer_size(&action->attr_list);
num_pebble_actions++;
}
}
int num_native_actions = (positive_action ? 1 : 0) + (negative_action ? 1 : 0);
int num_actions = num_native_actions + num_pebble_actions;
const int max_num_actions = 8; // Arbitratily chosen
uint8_t attributes_per_action[max_num_actions];
int action_idx = 0;
// Order of actions: ANCS positive, ANCS negative, Custom Pebble Actions
// The order the actions get populated in prv_populate_actions must be the same
if (positive_action) {
attributes_per_action[action_idx++] = NUM_NATIVE_ANCS_ACTION_ATTRS;
}
if (negative_action) {
attributes_per_action[action_idx++] = NUM_NATIVE_ANCS_ACTION_ATTRS;
}
if (notif_prefs) {
for (int i = 0; i < notif_prefs->action_group.num_actions; i++) {
TimelineItemAction *action = &notif_prefs->action_group.actions[i];
if (prv_should_hide_reply_because_group_sms(action, app_id, subtitle)) {
continue;
}
attributes_per_action[action_idx++] = action->attr_list.num_attributes;
}
}
int num_attr = ((title && title->length > 0 && !has_multimedia) ? 1 : 0) +
((display_name && display_name->length > 0) ? 1 : 0) +
((subtitle->length > 0) ? 1 : 0) +
((app_id->length > 0) ? 1 : 0) +
((message->length > 0 || has_multimedia) ? 1 : 0) +
(prv_should_add_sender_attr(app_id, title) ? 1 : 0) +
#if PBL_COLOR
((app_metadata->app_color != 0) ? 1 : 0) + // for color
#endif
1; // for icon
uint8_t *buffer;
TimelineItem *item = timeline_item_create(num_attr, num_actions, attributes_per_action,
required_space_for_strings, &buffer);
if (!item) {
// Out of memory - we do not croak on out of memory for notifications (PBL-10521)
PBL_LOG(LOG_LEVEL_WARNING, "Ignoring ANCS notification (out of memory)");
return NULL;
}
item->header.timestamp = timestamp;
prv_populate_attributes(item, &buffer, title, display_name, subtitle, message, app_id,
app_metadata, has_multimedia);
uint8_t *buf_end = buffer + required_space_for_strings;
prv_populate_actions(item, &buffer, buf_end, positive_action, negative_action, subtitle,
app_id, notif_prefs ? &notif_prefs->action_group : NULL, properties);
return item;
}
// Replace the dismiss action of a timeline item with the ancs negative action
void ancs_item_update_dismiss_action(TimelineItem *item, uint32_t uid,
const ANCSAttribute *attr_action_neg) {
TimelineItemAction *dismiss = timeline_item_find_dismiss_action(item);
if (dismiss) {
attribute_list_init_list(NUM_NATIVE_ANCS_ACTION_ATTRS + 1, &dismiss->attr_list);
uint8_t *string_buffer = NULL;
prv_fill_native_ancs_action(&string_buffer, dismiss, ActionIDNegative, attr_action_neg,
NULL, ANCSProperty_None);
// Add ancs ID as attribute since reminder's parent needs to be the associated pin
dismiss->attr_list.attributes[NUM_NATIVE_ANCS_ACTION_ATTRS].id = AttributeIdAncsId;
dismiss->attr_list.attributes[NUM_NATIVE_ANCS_ACTION_ATTRS].uint32 = uid;
// Copy the timeline item to move the new action back into the single buffer
TimelineItem *new_item = timeline_item_copy(item);
task_free(string_buffer);
attribute_list_destroy_list(&dismiss->attr_list);
timeline_item_free_allocated_buffer(item);
*item = *new_item;
new_item->allocated_buffer = NULL;
timeline_item_destroy(new_item);
}
}

View file

@ -0,0 +1,46 @@
/*
* 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 "ancs_notifications_util.h"
#include "comm/ble/kernel_le_client/ancs/ancs_types.h"
#include "services/normal/blob_db/ios_notif_pref_db.h"
#include "services/normal/timeline/timeline.h"
//! Creates a new timeline item from ANCS data
//! @param notif_attributes ANCS Notification attributes
//! @param app_attributes ANCS App attributes (namely, the display name)
//! @param app_metadata The icon and color associated with the app
//! @param notif_prefs iOS notification prefs for this notification
//! @param timestamp Time the notification occured
//! @param properties Additional ANCS properties (category, flags, etc)
//! @return The newly created timeline item
TimelineItem *ancs_item_create_and_populate(ANCSAttribute *notif_attributes[],
ANCSAttribute *app_attributes[],
const ANCSAppMetadata *app_metadata,
iOSNotifPrefs *notif_prefs,
time_t timestamp,
ANCSProperty properties);
//! Replaces the dismiss action of an existing timeline item with the ancs negative action
//! @param item The timeline item to update
//! @param uid The uid of the ANCS notification we're using the dismiss action from
//! @param attr_action_neg The negative action from the ANCS notification to use as the new
//! dismiss action
void ancs_item_update_dismiss_action(TimelineItem *item, uint32_t uid,
const ANCSAttribute *attr_action_neg);

View file

@ -0,0 +1,70 @@
/*
* 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.
*/
// @nolint
// please don't change these values manually, they are derived from the spreadsheet
// "Notification Colors"
#if PLATFORM_TINTIN
// Tintin does not have the color arg in its App Metadata. Remove it.
#define APP(id, icon, color) { id, icon }
#else
#define APP(id, icon, color) { id, icon, color }
#endif
APP(IOS_CALENDAR_APP_ID, TIMELINE_RESOURCE_TIMELINE_CALENDAR, GColorRedARGB8),
APP(IOS_FACETIME_APP_ID, TIMELINE_RESOURCE_NOTIFICATION_FACETIME, GColorIslamicGreenARGB8),
APP(IOS_MAIL_APP_ID, TIMELINE_RESOURCE_GENERIC_EMAIL, GColorVividCeruleanARGB8),
APP(IOS_PHONE_APP_ID, TIMELINE_RESOURCE_TIMELINE_MISSED_CALL, GColorPictonBlueARGB8),
APP(IOS_REMINDERS_APP_ID, TIMELINE_RESOURCE_NOTIFICATION_REMINDER, GColorFollyARGB8),
APP(IOS_SMS_APP_ID, TIMELINE_RESOURCE_GENERIC_SMS, GColorIslamicGreenARGB8),
APP("com.atebits.Tweetie2", TIMELINE_RESOURCE_NOTIFICATION_TWITTER, GColorVividCeruleanARGB8),
APP("com.burbn.instagram", TIMELINE_RESOURCE_NOTIFICATION_INSTAGRAM, GColorCobaltBlueARGB8),
APP("com.facebook.Facebook", TIMELINE_RESOURCE_NOTIFICATION_FACEBOOK, GColorCobaltBlueARGB8),
APP("com.facebook.Messenger", TIMELINE_RESOURCE_NOTIFICATION_FACEBOOK_MESSENGER, GColorBlueMoonARGB8),
APP("com.getpebble.pebbletime", TIMELINE_RESOURCE_NOTIFICATION_FLAG, GColorOrangeARGB8),
APP("com.google.calendar", TIMELINE_RESOURCE_TIMELINE_CALENDAR, GColorVeryLightBlueARGB8),
APP("com.google.Gmail", TIMELINE_RESOURCE_NOTIFICATION_GMAIL, GColorRedARGB8),
APP("com.google.hangouts", TIMELINE_RESOURCE_NOTIFICATION_GOOGLE_HANGOUTS, GColorJaegerGreenARGB8),
APP("com.google.inbox", TIMELINE_RESOURCE_NOTIFICATION_GOOGLE_INBOX, GColorBlueMoonARGB8),
APP("com.microsoft.Office.Outlook", TIMELINE_RESOURCE_NOTIFICATION_OUTLOOK, GColorCobaltBlueARGB8),
APP("com.orchestra.v2", TIMELINE_RESOURCE_NOTIFICATION_MAILBOX, GColorVividCeruleanARGB8),
APP("com.skype.skype", TIMELINE_RESOURCE_NOTIFICATION_SKYPE, GColorVividCeruleanARGB8),
APP("com.tapbots.Tweetbot3", TIMELINE_RESOURCE_NOTIFICATION_TWITTER, GColorVividCeruleanARGB8),
APP("com.toyopagroup.picaboo", TIMELINE_RESOURCE_NOTIFICATION_SNAPCHAT, GColorIcterineARGB8),
APP("com.yahoo.Aerogram", TIMELINE_RESOURCE_NOTIFICATION_YAHOO_MAIL, GColorIndigoARGB8),
APP("jp.naver.line", TIMELINE_RESOURCE_NOTIFICATION_LINE, GColorIslamicGreenARGB8),
APP("net.whatsapp.WhatsApp", TIMELINE_RESOURCE_NOTIFICATION_WHATSAPP, GColorIslamicGreenARGB8),
APP("ph.telegra.Telegraph", TIMELINE_RESOURCE_NOTIFICATION_TELEGRAM, GColorVividCeruleanARGB8),
#if !PLATFORM_TINTIN
APP("com.blackberry.bbm1", TIMELINE_RESOURCE_NOTIFICATION_BLACKBERRY_MESSENGER, GColorDarkGrayARGB8),
APP("com.getpebble.pebbletime.enterprise", TIMELINE_RESOURCE_NOTIFICATION_FLAG, GColorOrangeARGB8),
APP("com.google.GoogleMobile", TIMELINE_RESOURCE_NOTIFICATION_GENERIC, GColorBlueMoonARGB8),
APP("com.google.ios.youtube", TIMELINE_RESOURCE_NOTIFICATION_GENERIC, GColorClearARGB8),
APP("com.hipchat.ios", TIMELINE_RESOURCE_NOTIFICATION_HIPCHAT, GColorCobaltBlueARGB8),
APP("com.iwilab.KakaoTalk", TIMELINE_RESOURCE_NOTIFICATION_KAKAOTALK, GColorYellowARGB8),
APP("com.kik.chat", TIMELINE_RESOURCE_NOTIFICATION_KIK, GColorIslamicGreenARGB8),
APP("com.tencent.xin", TIMELINE_RESOURCE_NOTIFICATION_WECHAT, GColorKellyGreenARGB8),
APP("com.viber", TIMELINE_RESOURCE_NOTIFICATION_VIBER, GColorVividVioletARGB8),
APP("com.amazon.Amazon", TIMELINE_RESOURCE_NOTIFICATION_AMAZON, GColorChromeYellowARGB8),
APP("com.google.Maps", TIMELINE_RESOURCE_NOTIFICATION_GOOGLE_MAPS, GColorBlueMoonARGB8),
APP("com.google.photos", TIMELINE_RESOURCE_NOTIFICATION_GOOGLE_PHOTOS, GColorBlueMoonARGB8),
APP("com.apple.mobileslideshow", TIMELINE_RESOURCE_NOTIFICATION_IOS_PHOTOS, GColorBlueMoonARGB8),
APP("com.linkedin.LinkedIn", TIMELINE_RESOURCE_NOTIFICATION_LINKEDIN, GColorCobaltBlueARGB8),
APP("com.tinyspeck.chatlyio", TIMELINE_RESOURCE_NOTIFICATION_SLACK, GColorFollyARGB8),
#endif
#undef APP

View file

@ -0,0 +1,391 @@
/*
* 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 "ancs_notifications.h"
#include "ancs_filtering.h"
#include "ancs_item.h"
#include "ancs_phone_call.h"
#include "ancs_notifications_util.h"
#include "nexmo.h"
#include "comm/ble/kernel_le_client/ancs/ancs_types.h"
#include "drivers/rtc.h"
#include "kernel/pbl_malloc.h"
#include "services/common/analytics/analytics.h"
#include "services/common/i18n/i18n.h"
#include "services/normal/blob_db/ios_notif_pref_db.h"
#include "services/normal/blob_db/pin_db.h"
#include "services/normal/blob_db/reminder_db.h"
#include "services/normal/notifications/notification_storage.h"
#include "services/normal/notifications/notifications.h"
#include "services/normal/timeline/timeline.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/pstring.h"
#include <stdio.h>
static const Uuid uuid_reminders_data_source = UUID_REMINDERS_DATA_SOURCE;
static void prv_dismiss_notification(const TimelineItem *notification) {
const TimelineItemAction *action = timeline_item_find_dismiss_action(notification);
if (action) {
timeline_invoke_action(notification, action, NULL);
} else {
char uuid_buffer[UUID_STRING_BUFFER_LENGTH];
uuid_to_string(&notification->header.id, uuid_buffer);
PBL_LOG(LOG_LEVEL_ERROR, "Failed to load action for dismissal from %s", uuid_buffer);
}
}
static void prv_handle_new_ancs_notif(TimelineItem *notification) {
uuid_generate(&notification->header.id);
notifications_add_notification(notification);
timeline_item_destroy(notification);
}
static void prv_handle_ancs_update(TimelineItem *notification,
CommonTimelineItemHeader *existing_header) {
if (existing_header->dismissed) {
// this should be dismissed from iOS. Dismiss it again
PBL_LOG(LOG_LEVEL_DEBUG,
"ANCS notification already dismissed, dismissing again: %"PRIu32,
notification->header.ancs_uid);
prv_dismiss_notification(notification);
}
notification->header.status = existing_header->status;
notification->header.id = existing_header->id;
// Replace existing version of the notification
notification_storage_remove(&notification->header.id);
notification_storage_store(notification);
// we won't use this anywhere, free up the memory
timeline_item_destroy(notification);
}
static time_t prv_get_timestamp_from_ancs_date(const ANCSAttribute *date,
const ANCSAttribute *app_id) {
time_t timestamp = ancs_notifications_util_parse_timestamp(date);
if (timestamp == 0) {
// Another ANCS / iOS quirk, some apps (i.e. the Phone app)
// send an invalid-length string as date...
// Apple rdar://19639333
timestamp = rtc_get_time();
// copy out app ID to a char buffer
char app_id_buffer[app_id->length + 1];
pstring_pstring16_to_string(&app_id->pstr, app_id_buffer);
PBL_LOG(LOG_LEVEL_WARNING, "No valid date. Offending iOS app: %s", app_id_buffer);
}
return timestamp;
}
static bool prv_should_ignore_because_calendar_reminder(const ANCSAttribute *app_id) {
// do not show calendar notifications if we have reminders set (PBL-13271)
return pstring_equal_cstring(&app_id->pstr, IOS_CALENDAR_APP_ID) && !reminder_db_is_empty();
}
static bool prv_reminder_filter(SerializedTimelineItemHeader *hdr, void *context) {
// Check that the data source is the reminders app
TimelineItem pin;
pin_db_read_item_header(&pin, &hdr->common.parent_id);
if (uuid_equal(&pin.header.parent_id, &uuid_reminders_data_source)) {
return true;
}
return false;
}
static bool prv_should_ignore_because_time_reminder(const ANCSAttribute *app_id,
time_t timestamp,
const ANCSAttribute *title,
uint32_t uid,
const ANCSAttribute *attr_action_neg) {
if (!pstring_equal_cstring(&app_id->pstr, IOS_REMINDERS_APP_ID)) {
return false;
}
// copy out reminder title to a char buffer
char reminder_title_buffer[title->length + 1];
pstring_pstring16_to_string(&title->pstr, reminder_title_buffer);
TimelineItem reminder;
// If we found an existing reminder, replace its dismiss action with ancs negative action
if (reminder_db_find_by_timestamp_title(timestamp, reminder_title_buffer, prv_reminder_filter,
&reminder)) {
ancs_item_update_dismiss_action(&reminder, uid, attr_action_neg);
// Overwrite the existing item and notify system that reminder was updated
reminder_db_insert_item(&reminder);
timeline_item_free_allocated_buffer(&reminder);
return true;
}
return false;
}
static bool prv_find_existing_notification(TimelineItem *notification,
CommonTimelineItemHeader *existing_header_out) {
// PBL-9509: iOS' Calendar app uses the timestamp of the ANCS notification for the time of the
// event, not the time the notification was sent. If a calendar event has multiple notifications,
// for example, 15 mins before and 5 minutes before, ANCS will send 2x ANCS notifications.
// Because our dupe detection is based on the ANCS timestamp field, it filters out any calendar
// event reminder following the initial one. Therefore, skip dupe filtering for ANCS
// notifications from the calendar app.
// Also note that the 1st will be "removed" by ANCS as the 2nd gets sent out. Therefore, we do not
// need to do anything special to "clean up" any previous reminders for the same event.
if (notification->header.layout == LayoutIdCalendar) {
return false;
}
// Check if the notification is a duplicate/update
return notification_storage_find_ancs_notification_by_timestamp(notification,
existing_header_out);
}
static bool prv_should_ignore_because_duplicate(TimelineItem *notification,
CommonTimelineItemHeader *existing_header) {
return (notification->header.ancs_uid == existing_header->ancs_uid);
}
static bool prv_should_ignore_because_apple_mail_dot_app_bug(const ANCSAttribute *app_id,
const ANCSAttribute *message) {
// Apple's Mail.app sometimes sends a notification with "Loading..." or
// "This message has no content." when Mail.app is still fetching the body of the email.
// PBL-8407 / PBL-1090 / Apple bug report number: rdr://17851582
// Obviously this only works around the issue when the language is set to English.
if (!pstring_equal_cstring(&app_id->pstr, IOS_MAIL_APP_ID)) {
return false;
}
static const char loading_str[] = "Loading\xe2\x80\xa6";
if (pstring_equal_cstring(&message->pstr, loading_str)) {
return true;
}
static const char no_content_str[] = "This message has no content.";
if (pstring_equal_cstring(&message->pstr, no_content_str)) {
return true;
}
return false;
}
static bool prv_should_ignore_because_stale(time_t timestamp) {
static const time_t MAXIMUM_NOTIFY_TIME = 2 * 60 * 60; // 2 hours
static const time_t INVALID_TIME = ~0;
const time_t now = rtc_get_time();
// workaround for PBL-8400 (ignore notifications older than 2 hours)
// PBL-9066: Increased to 20 minutes due to Mail.app only fetching emails every 15 minutes
// PBL-9251: Increased to 2 hours. People have Fetch set to hourly.
// PBL-12726: Added a check to see if the timstamp is coming from a location based reminder
// This work-around is causing more trouble than the problem it was solving...
if (timestamp < (now - MAXIMUM_NOTIFY_TIME) && timestamp != INVALID_TIME) {
PBL_LOG(LOG_LEVEL_INFO, "Not presenting stale notif (ts=%ld)", timestamp);
return true;
}
return false;
}
static bool prv_should_ignore_because_muted(const iOSNotifPrefs *app_notif_prefs) {
return ancs_filtering_is_muted(app_notif_prefs);
}
static bool prv_should_ignore_notification(uint32_t uid,
time_t timestamp,
ANCSAttribute **notif_attributes,
iOSNotifPrefs *app_notif_prefs) {
const ANCSAttribute *app_id = notif_attributes[FetchedNotifAttributeIndexAppID];
const ANCSAttribute *message = notif_attributes[FetchedNotifAttributeIndexMessage];
const ANCSAttribute *title = notif_attributes[FetchedNotifAttributeIndexTitle];
const ANCSAttribute *negative_action =
notif_attributes[FetchedNotifAttributeIndexNegativeActionLabel];
if (prv_should_ignore_because_muted(app_notif_prefs)) {
char app_id_buffer[app_id->length + 1];
pstring_pstring16_to_string(&app_id->pstr, app_id_buffer);
PBL_LOG(LOG_LEVEL_INFO, "Ignoring notification from <%s>: Muted", app_id_buffer);
analytics_inc(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_FILTERED_BECAUSE_MUTED_COUNT,
AnalyticsClient_System);
return true;
}
// filter out mail buggy mail messages
if (prv_should_ignore_because_apple_mail_dot_app_bug(app_id, message)) {
PBL_LOG(LOG_LEVEL_ERROR, "Ignoring ANCS notification because Mail.app bug");
return true;
}
// Calendar and time-based Reminders app reminders are handled through the mobile app and are
// added to reminder_db, so we filter them out to avoid doubling up. Notifications from other
// apps and location-based reminder are handled as regular notifications.
// filter out extraneous calendar messages
if (prv_should_ignore_because_calendar_reminder(app_id)) {
PBL_LOG(LOG_LEVEL_DEBUG, "Ignoring ANCS calendar notification because reminders are set");
return true;
}
// filter out time based reminder notifications
if (prv_should_ignore_because_time_reminder(app_id, timestamp, title, uid,
negative_action)) {
PBL_LOG(LOG_LEVEL_DEBUG, "Ignoring ANCS reminders notification because existing "
"time-based reminder was found in db");
return true;
}
// filter out super-old notifications
if (prv_should_ignore_because_stale(timestamp)) {
return true;
}
return false;
}
void ancs_notifications_handle_message(uint32_t uid,
ANCSProperty properties,
ANCSAttribute **notif_attributes,
ANCSAttribute **app_attributes) {
PBL_ASSERTN(notif_attributes && app_attributes);
const ANCSAttribute *app_id = notif_attributes[FetchedNotifAttributeIndexAppID];
if (!app_id || app_id->length == 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Can't handle notifications without an app id");
return;
}
const ANCSAttribute *title = notif_attributes[FetchedNotifAttributeIndexTitle];
const ANCSAttribute *subtitle = notif_attributes[FetchedNotifAttributeIndexSubtitle];
const ANCSAttribute *display_name = app_attributes[FetchedAppAttributeIndexDisplayName];
const ANCSAttribute *date = notif_attributes[FetchedNotifAttributeIndexDate];
const ANCSAttribute *message = notif_attributes[FetchedNotifAttributeIndexMessage];
iOSNotifPrefs *app_notif_prefs = ios_notif_pref_db_get_prefs(app_id->value, app_id->length);
ancs_filtering_record_app(&app_notif_prefs, app_id, display_name, title);
if (nexmo_is_reauth_sms(app_id, message)) {
nexmo_handle_reauth_sms(uid, app_id, message, app_notif_prefs);
goto cleanup;
}
const time_t timestamp = prv_get_timestamp_from_ancs_date(date, app_id);
if (prv_should_ignore_notification(uid, timestamp, notif_attributes, app_notif_prefs)) {
goto cleanup;
}
// If this is an incoming call, let the phone service handle it
// It would be nice to handle facetime calls with the phone service but it doesn't look like we
// can: https://pebbletechnology.atlassian.net/browse/PBL-16955
const bool is_notification_from_phone_app = ancs_notifications_util_is_phone(app_id);
const bool has_incoming_call_property = (properties & ANCSProperty_IncomingCall);
const bool has_missed_call_property = (properties & ANCSProperty_MissedCall);
if (is_notification_from_phone_app) {
if (has_incoming_call_property) {
ancs_phone_call_handle_incoming(uid, properties, notif_attributes);
goto cleanup;
}
// When declining a phone call from the Phone UI we still get a missed call notification
// with a different UID. We don't want to show a missed call notification / pin in this case.
if (has_missed_call_property && ancs_phone_call_should_ignore_missed_calls()) {
PBL_LOG(LOG_LEVEL_INFO, "Ignoring missed call");
goto cleanup;
}
}
const bool is_sms = ancs_notifications_util_is_sms(app_id);
if (is_sms) {
analytics_inc(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_SMS_COUNT,
AnalyticsClient_System);
}
if (ancs_notifications_util_is_group_sms(app_id, subtitle)) {
analytics_inc(ANALYTICS_DEVICE_METRIC_NOTIFICATION_ANCS_GROUP_SMS_COUNT,
AnalyticsClient_System);
}
// add a notification
const ANCSAppMetadata* app_metadata = ancs_notifications_util_get_app_metadata(app_id);
TimelineItem *notification = ancs_item_create_and_populate(notif_attributes, app_attributes,
app_metadata, app_notif_prefs,
timestamp, properties);
if (!notification) { goto cleanup; }
notification->header.ancs_uid = uid;
notification->header.type = TimelineItemTypeNotification;
notification->header.layout = LayoutIdNotification;
notification->header.ancs_notif = true;
notification_storage_lock();
// filter out duplicate notifications
CommonTimelineItemHeader existing_header;
if (prv_find_existing_notification(notification, &existing_header)) {
if (prv_should_ignore_because_duplicate(notification, &existing_header)) {
PBL_LOG(LOG_LEVEL_DEBUG, "Duplicate ANCS notification: %"PRIu32, uid);
timeline_item_destroy(notification);
notification_storage_unlock();
goto cleanup;
}
prv_handle_ancs_update(notification, &existing_header);
} else {
prv_handle_new_ancs_notif(notification);
}
notification_storage_unlock();
// if missed call, also add a pin
if (is_notification_from_phone_app && has_missed_call_property) {
TimelineItem *missed_call_pin =
ancs_item_create_and_populate(notif_attributes, app_attributes, app_metadata,
app_notif_prefs, timestamp, properties);
if (missed_call_pin == NULL) { goto cleanup; }
timeline_add_missed_call_pin(missed_call_pin, uid);
timeline_item_destroy(missed_call_pin);
}
cleanup:
ios_notif_pref_db_free_prefs(app_notif_prefs);
}
void ancs_notifications_handle_notification_removed(uint32_t ancs_uid, ANCSProperty properties) {
// Dismissal from phone is only properly supported on iOS 9 and up
// The presence of the DIS service tells us we have at least iOS 9
const bool ios_9 = (properties & ANCSProperty_iOS9);
if (!ios_9) {
return;
}
Uuid *notification_id = kernel_malloc_check(sizeof(Uuid));
if (notification_storage_find_ancs_notification_id(ancs_uid, notification_id)) {
PBL_LOG(LOG_LEVEL_DEBUG, "Notification removed from notification centre: (UID: %"PRIu32")",
ancs_uid);
notification_storage_set_status(notification_id, TimelineItemStatusDismissed);
notifications_handle_notification_acted_upon(notification_id);
} else {
// notification_id is passed into an event if a matching notification was found, so it will
// be freed by the system later
kernel_free(notification_id);
}
if (properties & ANCSProperty_IncomingCall) {
ancs_phone_call_handle_removed(ancs_uid, ios_9);
}
}

View file

@ -0,0 +1,30 @@
/*
* 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 <stdlib.h>
#include "comm/ble/kernel_le_client/ancs/ancs_types.h"
#include "services/normal/timeline/item.h"
void ancs_notifications_handle_message(uint32_t uid,
ANCSProperty properties,
ANCSAttribute **notif_attributes,
ANCSAttribute **app_attributes);
void ancs_notifications_handle_notification_removed(uint32_t ancs_uid, ANCSProperty properties);

View file

@ -0,0 +1,122 @@
/*
* 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 "ancs_notifications_util.h"
#include "drivers/rtc.h"
#include "resource/timeline_resource_ids.auto.h"
#include "system/passert.h"
#include "util/attributes.h"
#include "util/date.h"
#include "util/pstring.h"
#include "util/size.h"
#include "util/string.h"
const ANCSAppMetadata* ancs_notifications_util_get_app_metadata(const ANCSAttribute *app_id) {
static const ANCSAppMetadata s_generic_app = {
#if PBL_COLOR
.app_color = GColorClearARGB8,
#endif
.icon_id = TIMELINE_RESOURCE_NOTIFICATION_GENERIC,
};
static const struct ANCSAppMetadata map[] = {
#include "ancs_known_apps.h"
};
for (unsigned int index = 0; index < ARRAY_LENGTH(map); ++index) {
const struct ANCSAppMetadata *mapping = &map[index];
if (pstring_equal_cstring(&app_id->pstr, mapping->app_id)) {
return mapping;
}
}
// App ID doesn't match any of the known IDs:
return &s_generic_app;
}
time_t ancs_notifications_util_parse_timestamp(const ANCSAttribute *timestamp_attr) {
PBL_ASSERTN(timestamp_attr);
struct PACKED {
char year[4];
char month[2];
char day[2];
char T[1];
char hour[2];
char minute[2];
char second[2];
char Z[1];
} timestamp;
// Make sure the attribute is the length we expect and that it doesn't have random NULL
// characters in the middle
if (timestamp_attr->length < sizeof(timestamp) - 1 ||
strnlen((char *)timestamp_attr->value, sizeof(timestamp) - 1) < sizeof(timestamp) - 1) {
// invalid length
return 0;
}
memcpy(&timestamp, timestamp_attr->value, sizeof(timestamp) - 1);
timestamp.Z[0] = '\0';
if (timestamp.year[0] != '2' || timestamp.year[1] != '0') {
// invalid data, we have bigger fishes to fry than the year 2100 -FBO
return 0;
}
struct tm time_tm = { 0 };
time_tm.tm_sec = atoi(timestamp.second);
timestamp.second[0] = '\0';
time_tm.tm_min = atoi(timestamp.minute);
timestamp.minute[0] = '\0';
time_tm.tm_hour = atoi(timestamp.hour);
timestamp.T[0] = '\0';
time_tm.tm_mday = atoi(timestamp.day);
timestamp.day[0] = '\0';
time_tm.tm_mon = atoi(timestamp.month) - 1;
timestamp.month[0] = '\0';
time_tm.tm_year = atoi(timestamp.year) - STDTIME_YEAR_OFFSET;
// We have to assume that the timezone of the phone matches the timezone of the watch
time_t sys_time = rtc_get_time();
time_tm.tm_gmtoff = time_get_gmtoffset();
time_get_timezone_abbr(time_tm.tm_zone, sys_time);
time_tm.tm_isdst = time_get_isdst(sys_time);
return mktime(&time_tm);
}
bool ancs_notifications_util_is_phone(const ANCSAttribute *app_id) {
return (app_id && pstring_equal_cstring(&app_id->pstr, IOS_PHONE_APP_ID));
}
bool ancs_notifications_util_is_sms(const ANCSAttribute *app_id) {
return (app_id && pstring_equal_cstring(&app_id->pstr, IOS_SMS_APP_ID));
}
bool ancs_notifications_util_is_group_sms(const ANCSAttribute *app_id,
const ANCSAttribute *subtitle) {
if (!ancs_notifications_util_is_sms(app_id)) {
return false;
}
// The defining feature of a group sms (vs a regular sms) is that it has a subtitle field
if (!subtitle || subtitle->length == 0) {
return false;
}
return true;
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "applib/graphics/gtypes.h"
#include "comm/ble/kernel_le_client/ancs/ancs_types.h"
#include "util/attributes.h"
#include "util/time/time.h"
#define IOS_PHONE_APP_ID "com.apple.mobilephone"
#define IOS_CALENDAR_APP_ID "com.apple.mobilecal"
#define IOS_REMINDERS_APP_ID "com.apple.reminders"
#define IOS_MAIL_APP_ID "com.apple.mobilemail"
#define IOS_SMS_APP_ID "com.apple.MobileSMS"
#define IOS_FACETIME_APP_ID "com.apple.facetime"
typedef struct PACKED ANCSAppMetadata {
const char *app_id;
uint32_t icon_id;
#if PBL_COLOR
uint8_t app_color;
#endif
bool is_blocked:1; //<! Whether the app's notifications should always be ignored
bool is_unblockable:1; //<! Whether the app's notifications should never be ignored
} ANCSAppMetadata;
const ANCSAppMetadata* ancs_notifications_util_get_app_metadata(const ANCSAttribute *app_id);
time_t ancs_notifications_util_parse_timestamp(const ANCSAttribute *timestamp_attr);
// Returns true if given app id is the iOS phone app
bool ancs_notifications_util_is_phone(const ANCSAttribute *app_id);
// Returns true if given app id is the iOS sms app
bool ancs_notifications_util_is_sms(const ANCSAttribute *app_id);
// Returns true if given app id and subtitle denote a group sms
bool ancs_notifications_util_is_group_sms(const ANCSAttribute *app_id,
const ANCSAttribute *subtitle);

View file

@ -0,0 +1,108 @@
/*
* 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 "ancs_phone_call.h"
#include "applib/graphics/utf8.h"
#include "kernel/events.h"
#include "services/common/regular_timer.h"
#include "services/normal/phone_call_util.h"
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
static RegularTimerInfo s_missed_call_timer_id;
static void prv_put_call_event(PhoneEventType type, uint32_t call_identifier,
PebblePhoneCaller *caller, bool ios_9) {
PebbleEvent event = {
.type = PEBBLE_PHONE_EVENT,
.phone = {
.type = type,
.source = ios_9 ? PhoneCallSource_ANCS : PhoneCallSource_ANCS_Legacy,
.call_identifier = call_identifier,
.caller = caller,
}
};
event_put(&event);
}
static void prv_strip_formatting_chars(char *text) {
const size_t len = strlen(text);
utf8_t *w_cursor = (uint8_t *)text;
utf8_t *r_cursor = (uint8_t *)text;
utf8_t *next;
uint32_t codepoint;
while (true) {
codepoint = utf8_peek_codepoint(r_cursor, &next);
if (codepoint == 0) {
break;
}
if (!codepoint_is_formatting_indicator(codepoint)) {
uint8_t c_len = utf8_copy_character(w_cursor, r_cursor, len);
w_cursor += c_len;
}
r_cursor = next;
}
*w_cursor = '\0';
}
void ancs_phone_call_handle_incoming(uint32_t uid, ANCSProperty properties,
ANCSAttribute **notif_attributes) {
// This field holds the caller's name if the phone number belongs to a contact,
// or the actual phone number if it does not belong to a contact
const ANCSAttribute *caller_id = notif_attributes[FetchedNotifAttributeIndexTitle];
char caller_id_str[caller_id->length + 1];
pstring_pstring16_to_string(&caller_id->pstr, caller_id_str);
prv_strip_formatting_chars(caller_id_str);
PebblePhoneCaller *caller = phone_call_util_create_caller(caller_id_str, NULL);
const bool ios_9 = (properties & ANCSProperty_iOS9);
prv_put_call_event(PhoneEventType_Incoming, uid, caller, ios_9);
}
void ancs_phone_call_handle_removed(uint32_t uid, bool ios_9) {
prv_put_call_event(PhoneEventType_Hide, uid, NULL, ios_9);
}
bool ancs_phone_call_should_ignore_missed_calls(void) {
return regular_timer_is_scheduled(&s_missed_call_timer_id);
}
static void prv_handle_missed_call_timer_timeout(void *not_used) {
if (regular_timer_is_scheduled(&s_missed_call_timer_id)) {
regular_timer_remove_callback(&s_missed_call_timer_id);
}
}
void ancs_phone_call_temporarily_block_missed_calls(void) {
const int BLOCK_MISS_CALL_TIME_S = 7;
if (regular_timer_is_scheduled(&s_missed_call_timer_id)) {
regular_timer_remove_callback(&s_missed_call_timer_id);
}
s_missed_call_timer_id = (const RegularTimerInfo) {
.cb = prv_handle_missed_call_timer_timeout,
};
regular_timer_add_multisecond_callback(&s_missed_call_timer_id, BLOCK_MISS_CALL_TIME_S);
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "comm/ble/kernel_le_client/ancs/ancs_types.h"
//! Puts an incoming call event
//! @param uid ANCS UID of the incoming call notification
//! @param properties ANCS properties provided by the ANCS client
//! @param notif_attributes The notification attributes containing things such as caller id
void ancs_phone_call_handle_incoming(uint32_t uid, ANCSProperty properties,
ANCSAttribute **notif_attributes);
//! Puts a hide call event - used in response to an ANCS removal message
//! @param uid ANCS UID of the removed incoming call notification
//! @param ios_9 Whether or not this notification was from an iOS 9 device
void ancs_phone_call_handle_removed(uint32_t uid, bool ios_9);
//! Returns true if we're currently ignoring missed calls (to avoid unnecessary notifications after
//! declining a call)
bool ancs_phone_call_should_ignore_missed_calls(void);
//! Blocks missed calls for a predetermined amount of time (called when dismissing a call from
//! the phone UI)
void ancs_phone_call_temporarily_block_missed_calls(void);

View file

@ -0,0 +1,65 @@
/*
* 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 "nexmo.h"
#include "ancs_notifications_util.h"
#include "comm/ble/kernel_le_client/ancs/ancs.h"
#include "comm/ble/kernel_le_client/ancs/ancs_types.h"
#include "system/logging.h"
T_STATIC char* NEXMO_REAUTH_STRING = "Pebble check-in code:";
bool nexmo_is_reauth_sms(const ANCSAttribute *app_id, const ANCSAttribute *message) {
if (ancs_notifications_util_is_sms(app_id)) {
if (strstr((const char *)message->value, NEXMO_REAUTH_STRING)) {
PBL_LOG(LOG_LEVEL_INFO, "Got Nexmo Reauth SMS");
return true;
}
}
return false;
}
void nexmo_handle_reauth_sms(uint32_t uid,
const ANCSAttribute *app_id,
const ANCSAttribute *message,
iOSNotifPrefs *existing_notif_prefs) {
const int num_existing_attributes = existing_notif_prefs ?
existing_notif_prefs->attr_list.num_attributes : 0;
AttributeList new_attr_list;
attribute_list_init_list(num_existing_attributes, &new_attr_list);
// Copy over all the existing attributes to our new list
if (existing_notif_prefs) {
for (int i = 0; i < num_existing_attributes; i++) {
new_attr_list.attributes[i] = existing_notif_prefs->attr_list.attributes[i];
}
}
char msg_buffer[message->length + 1];
memcpy(msg_buffer, message->value, message->length);
msg_buffer[message->length] = '\0';
attribute_list_add_cstring(&new_attr_list, AttributeIdAuthCode, msg_buffer);
// This will trigger a sync sending the auth code to the phone
ios_notif_pref_db_store_prefs(app_id->value, app_id->length,
&new_attr_list, &existing_notif_prefs->action_group);
// Dismiss the notification so the user is oblivious to this process
ancs_perform_action(uid, ActionIDNegative);
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "comm/ble/kernel_le_client/ancs/ancs_types.h"
#include "services/normal/blob_db/ios_notif_pref_db.h"
bool nexmo_is_reauth_sms(const ANCSAttribute *app_id, const ANCSAttribute *message);
//! Adds the reauth msg to the notif prefs so the phone can start the reauth process
void nexmo_handle_reauth_sms(uint32_t uid,
const ANCSAttribute *app_id,
const ANCSAttribute *message,
iOSNotifPrefs *existing_notif_prefs);

View file

@ -0,0 +1,354 @@
/*
* 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 "do_not_disturb.h"
#include "do_not_disturb_toggle.h"
#include "applib/ui/action_toggle.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/dialogs/actionable_dialog.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/dialogs/expandable_dialog.h"
#include "applib/ui/dialogs/simple_dialog.h"
#include "applib/ui/vibes.h"
#include "applib/ui/window_manager.h"
#include "applib/ui/window_stack.h"
#include "drivers/rtc.h"
#include "kernel/events.h"
#include "kernel/ui/modals/modal_manager.h"
#include "process_state/app_state/app_state.h"
#include "resource/resource_ids.auto.h"
#include "services/common/analytics/analytics.h"
#include "services/common/i18n/i18n.h"
#include "services/common/new_timer/new_timer.h"
#include "services/common/system_task.h"
#include "services/normal/activity/activity.h"
#include "services/normal/notifications/alerts_preferences.h"
#include "services/normal/notifications/alerts_preferences_private.h"
#include "services/normal/timeline/calendar.h"
#include "syscall/syscall_internal.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/list.h"
#include "util/math.h"
#include "util/time/time.h"
#include <stdbool.h>
#include <string.h>
typedef struct DoNotDisturbData {
TimerID update_timer_id;
bool is_in_schedule_period;
bool manually_override_dnd;
bool was_active;
} DoNotDisturbData;
static DoNotDisturbData s_data;
static bool prv_is_smart_dnd_active(void);
static bool prv_is_schedule_active(void);
static void prv_set_schedule_mode_timer();
static void prv_update_active_time(bool is_active) {
if (is_active) {
analytics_stopwatch_start(ANALYTICS_DEVICE_METRIC_ALERTS_DND_ACTIVE_TIME,
AnalyticsClient_System);
} else {
analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_ALERTS_DND_ACTIVE_TIME);
}
}
static void prv_put_dnd_event(bool is_active) {
PebbleEvent e = (PebbleEvent) {
.type = PEBBLE_DO_NOT_DISTURB_EVENT,
.do_not_disturb = {
.is_active = is_active,
}
};
event_put(&e);
}
static char *prv_bool_to_string(bool active) {
return active ? "Active" : "Inactive";
}
static void prv_do_update(void) {
const bool is_active = do_not_disturb_is_active();
if (is_active == s_data.was_active) {
// No change
return;
}
s_data.was_active = is_active;
PBL_LOG(LOG_LEVEL_INFO, "Quiet Time: %s", prv_bool_to_string(is_active));
prv_update_active_time(is_active);
prv_put_dnd_event(is_active);
}
static void prv_toggle_smart_dnd(void *e_dialog) {
alerts_preferences_dnd_set_smart_enabled(!alerts_preferences_dnd_is_smart_enabled());
s_data.manually_override_dnd = false;
prv_do_update();
}
static void prv_toggle_manual_dnd_from_action_menu(void *e_dialog) {
do_not_disturb_toggle_push(ActionTogglePrompt_NoPrompt, false /* set_exit_reason */);
}
static void prv_toggle_manual_dnd_from_settings_menu(void *e_dialog) {
do_not_disturb_set_manually_enabled(!do_not_disturb_is_manually_enabled());
}
static void prv_push_first_use_dialog(const char* msg,
DialogCallback dialog_close_cb) {
DialogCallbacks callbacks = { .unload = dialog_close_cb };
ExpandableDialog *first_use_dialog = expandable_dialog_create_with_params(
"DNDFirstUse", RESOURCE_ID_QUIET_TIME, msg, GColorBlack, GColorMediumAquamarine,
&callbacks, RESOURCE_ID_ACTION_BAR_ICON_CHECK, expandable_dialog_close_cb);
i18n_free(msg, &s_data);
expandable_dialog_push(first_use_dialog,
window_manager_get_window_stack(ModalPriorityNotification));
}
static void prv_push_smart_dnd_first_use_dialog(void) {
const char *msg = i18n_get("Calendar Aware enables Quiet Time automatically during " \
"calendar events.", &s_data);
prv_push_first_use_dialog(msg, prv_toggle_smart_dnd);
}
static void prv_push_manual_dnd_first_use_dialog(ManualDNDFirstUseSource source) {
const char *msg = i18n_get("Press and hold the Back button from a notification to turn " \
"Quiet Time on or off.", &s_data);
if (source == ManualDNDFirstUseSourceActionMenu) {
prv_push_first_use_dialog(msg, prv_toggle_manual_dnd_from_action_menu);
} else {
prv_push_first_use_dialog(msg, prv_toggle_manual_dnd_from_settings_menu);
}
}
static void prv_try_update_schedule_mode(void *data) {
const bool clear_override = (bool) (uintptr_t) data;
if (clear_override) {
s_data.manually_override_dnd = false;
}
if (do_not_disturb_is_schedule_enabled(WeekdaySchedule) ||
do_not_disturb_is_schedule_enabled(WeekendSchedule)) {
prv_set_schedule_mode_timer();
} else {
new_timer_stop(s_data.update_timer_id);
s_data.is_in_schedule_period = false;
}
prv_do_update();
}
static void prv_try_update_schedule_mode_callback(bool clear_manual_override) {
system_task_add_callback(prv_try_update_schedule_mode, (void*)(uintptr_t) clear_manual_override);
}
static void prv_update_schedule_mode_timer_callback(void* not_used) {
prv_try_update_schedule_mode_callback(true);
}
static DoNotDisturbScheduleType prv_current_schedule_type(void) {
struct tm time;
rtc_get_time_tm(&time);
return ((time.tm_wday == Saturday || time.tm_wday == Sunday) ?
WeekendSchedule : WeekdaySchedule);
}
// Updates the timer for scheduled DND check
// Only enters if at least one of the schedules is enabled
static void prv_set_schedule_mode_timer() {
struct tm time;
rtc_get_time_tm(&time);
DoNotDisturbScheduleType curr_schedule_type = prv_current_schedule_type();
DoNotDisturbSchedule curr_schedule;
do_not_disturb_get_schedule(curr_schedule_type, &curr_schedule);
bool curr_schedule_enabled = do_not_disturb_is_schedule_enabled(curr_schedule_type);
time_t seconds_until_update;
bool is_enable_next;
int curr_day = time.tm_wday;
if (!curr_schedule_enabled) { // Only next schedule is enabled
is_enable_next = true;
// Depending on the current schedule, determine the first day index of the next schedule
int next_schedule_day = (curr_schedule_type == WeekdaySchedule) ? Saturday : Monday;
// Count the number of full days until next schedule (Sunday = 0)
int num_full_days = ((next_schedule_day - curr_day + DAYS_PER_WEEK) % DAYS_PER_WEEK) - 1;
// Calculate the number of seconds until the start of the next schedule, update then
seconds_until_update = time_util_get_seconds_until_daily_time(&time, 0, 0) +
(num_full_days * SECONDS_PER_DAY);
} else { // Current schedule is enabled
const time_t seconds_until_start = time_util_get_seconds_until_daily_time(
&time, curr_schedule.from_hour, curr_schedule.from_minute);
const time_t seconds_until_end = time_util_get_seconds_until_daily_time(
&time, curr_schedule.to_hour, curr_schedule.to_minute);
seconds_until_update = MIN(seconds_until_start, seconds_until_end);
is_enable_next = (seconds_until_update == seconds_until_start);
// Update at midnight if on the last day of the current schedule
if ((curr_day == Sunday) || (curr_day == Friday)) {
const time_t seconds_until_midnight = time_util_get_seconds_until_daily_time(&time, 0, 0);
seconds_until_update = MIN(seconds_until_update, seconds_until_midnight);
}
}
if (s_data.is_in_schedule_period == is_enable_next) {
// Coming out of scheduled DND with manual DND on, turning it off
if (is_enable_next && do_not_disturb_is_manually_enabled()) {
do_not_disturb_set_manually_enabled(false);
}
s_data.is_in_schedule_period = !is_enable_next;
}
PBL_LOG(LOG_LEVEL_INFO, "%s scheduled period. %u seconds until update",
s_data.is_in_schedule_period ? "In" : "Out of", (unsigned int) seconds_until_update);
bool success = new_timer_start(s_data.update_timer_id, seconds_until_update * 1000,
prv_update_schedule_mode_timer_callback, NULL, 0 /*flags*/);
PBL_ASSERTN(success);
}
static bool prv_is_current_schedule_enabled() {
return (do_not_disturb_is_schedule_enabled(prv_current_schedule_type()));
}
static bool prv_is_schedule_active(void) {
return (prv_is_current_schedule_enabled() && s_data.is_in_schedule_period &&
!s_data.manually_override_dnd);
}
static bool prv_is_smart_dnd_active(void) {
return (calendar_event_is_ongoing() &&
do_not_disturb_is_smart_dnd_enabled() &&
!s_data.manually_override_dnd);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
//! Public Functions
///////////////////////////////////////////////////////////////////////////////////////////////////
DEFINE_SYSCALL(bool, sys_do_not_disturb_is_active, void) {
return do_not_disturb_is_active();
}
bool do_not_disturb_is_active(void) {
if (do_not_disturb_is_manually_enabled() ||
prv_is_schedule_active() ||
prv_is_smart_dnd_active()) {
return true;
}
return false;
}
bool do_not_disturb_is_manually_enabled(void) {
return alerts_preferences_dnd_is_manually_enabled();
}
void do_not_disturb_set_manually_enabled(bool enable) {
const bool is_auto_dnd = prv_is_current_schedule_enabled() ||
do_not_disturb_is_smart_dnd_enabled();
const bool was_active = do_not_disturb_is_active();
alerts_preferences_dnd_set_manually_enabled(enable);
// Turning the manual DND OFF in an active DND mode overrides the automatic mode
if (!enable && was_active && is_auto_dnd) {
s_data.manually_override_dnd = true;
}
prv_do_update();
}
void do_not_disturb_toggle_manually_enabled(ManualDNDFirstUseSource source) {
FirstUseSource first_use_source = (FirstUseSource)source;
if (!alerts_preferences_check_and_set_first_use_complete(first_use_source)) {
prv_push_manual_dnd_first_use_dialog(source);
} else {
if (source == ManualDNDFirstUseSourceSettingsMenu) {
prv_toggle_manual_dnd_from_settings_menu(NULL);
} else {
prv_toggle_manual_dnd_from_action_menu(NULL);
}
}
}
bool do_not_disturb_is_smart_dnd_enabled(void) {
return alerts_preferences_dnd_is_smart_enabled();
}
void do_not_disturb_toggle_smart_dnd(void) {
if (!alerts_preferences_check_and_set_first_use_complete(FirstUseSourceSmartDND)) {
prv_push_smart_dnd_first_use_dialog();
} else {
prv_toggle_smart_dnd(NULL);
}
}
void do_not_disturb_get_schedule(DoNotDisturbScheduleType type,
DoNotDisturbSchedule *schedule_out) {
alerts_preferences_dnd_get_schedule(type, schedule_out);
}
void do_not_disturb_set_schedule(DoNotDisturbScheduleType type, DoNotDisturbSchedule *schedule) {
alerts_preferences_dnd_set_schedule(type, schedule);
prv_try_update_schedule_mode_callback(true);
}
bool do_not_disturb_is_schedule_enabled(DoNotDisturbScheduleType type) {
return alerts_preferences_dnd_is_schedule_enabled(type);
}
void do_not_disturb_set_schedule_enabled(DoNotDisturbScheduleType type, bool scheduled) {
alerts_preferences_dnd_set_schedule_enabled(type, scheduled);
prv_try_update_schedule_mode_callback(true);
}
void do_not_disturb_toggle_scheduled(DoNotDisturbScheduleType type) {
alerts_preferences_dnd_set_schedule_enabled(type,
!alerts_preferences_dnd_is_schedule_enabled(type));
prv_try_update_schedule_mode_callback(true);
}
void do_not_disturb_init(void) {
s_data = (DoNotDisturbData) {
.update_timer_id = new_timer_create(),
.was_active = false,
};
prv_try_update_schedule_mode((void*) true);
}
void do_not_disturb_handle_clock_change(void) {
prv_try_update_schedule_mode_callback(false);
}
void do_not_disturb_handle_calendar_event(PebbleCalendarEvent *e) {
prv_do_update();
}
void do_not_disturb_manual_toggle_with_dialog(void) {
do_not_disturb_toggle_push(ActionTogglePrompt_Auto, false /* set_exit_reason */);
}
#ifdef UNITTEST
TimerID get_dnd_timer_id(void) {
return s_data.update_timer_id;
}
void set_dnd_timer_id(TimerID id) {
s_data.update_timer_id = id;
}
#endif

View file

@ -0,0 +1,100 @@
/*
* 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/attributes.h"
#include "kernel/events.h"
#include "services/normal/notifications/alerts_preferences.h"
#include <inttypes.h>
#include <stdbool.h>
typedef enum DoNotDisturbScheduleType {
WeekdaySchedule,
WeekendSchedule,
NumDNDSchedules,
} DoNotDisturbScheduleType;
typedef struct PACKED DoNotDisturbSchedule {
uint8_t from_hour;
uint8_t from_minute;
uint8_t to_hour;
uint8_t to_minute;
} DoNotDisturbSchedule;
typedef enum ManualDNDFirstUseSource {
ManualDNDFirstUseSourceActionMenu = 0,
ManualDNDFirstUseSourceSettingsMenu
} ManualDNDFirstUseSource;
//! The Do Not Disturb service is meant for internal use only. Clients should use the Alerts
//! Service to determine how/when the user can be notified.
//! DND (Quiet Time) Activation Modes
//! Manual - Allows the user to quickly put the watch into an active DND mode. It
//! overrides other DND activation modes if toggled off. Once the watch comes out of scheduled DND,
//! manual DND automatically turns off.
//! Smart DND (Calendar Aware) - Leverages the calendar service to determine if an event is ongoing
//! and automatically puts the watch into an Active DND Mode
//! Scheduled DND - Allows the user to specify a daily schedule for when the DND should be in active
//! mode. Once coming out of a schedule, if the Manual DND is enabled, it disables that setting.
//! @return True if DND is in effect, false if not.
bool do_not_disturb_is_active(void);
//! Manual DND is a simple on / off switch for DND,
//! which works along side automatic modes (scheduled and calendar aware ('smart')
//! @return True if DND has been manually enabled, false if not.
bool do_not_disturb_is_manually_enabled(void);
//! Set the current manual DND state
void do_not_disturb_set_manually_enabled(bool enable);
//! Toggle the current manual DND state. Provide the source from which it was toggled.
//! Toggling from the settings menu simply toggles the Manual DND setting
//! Toggling from a notification action menu sets the Manual DND setting to opposite of the current
//! DND active state.
void do_not_disturb_toggle_manually_enabled(ManualDNDFirstUseSource source);
bool do_not_disturb_is_smart_dnd_enabled(void);
void do_not_disturb_toggle_smart_dnd(void);
void do_not_disturb_get_schedule(DoNotDisturbScheduleType type, DoNotDisturbSchedule *schedule_out);
void do_not_disturb_set_schedule(DoNotDisturbScheduleType type, DoNotDisturbSchedule *schedule);
bool do_not_disturb_is_schedule_enabled(DoNotDisturbScheduleType type);
void do_not_disturb_set_schedule_enabled(DoNotDisturbScheduleType type, bool scheduled);
void do_not_disturb_toggle_scheduled(DoNotDisturbScheduleType type);
void do_not_disturb_init(void);
void do_not_disturb_handle_clock_change(void);
void do_not_disturb_handle_calendar_event(PebbleCalendarEvent *e);
void do_not_disturb_manual_toggle_with_dialog(void);
#if UNITTEST
#include "services/common/new_timer/new_timer.h"
TimerID get_dnd_timer_id(void);
void set_dnd_timer_id(TimerID id);
#endif

View file

@ -0,0 +1,57 @@
/*
* 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 "do_not_disturb.h"
#include "do_not_disturb_toggle.h"
#include "applib/app_exit_reason.h"
#include "applib/ui/action_toggle.h"
#include "services/common/i18n/i18n.h"
#include "system/logging.h"
static bool prv_get_state(void *context) {
// This toggle does not necessarily toggle Manual DND. It sets Manual DND to the opposite of DND
// active status which in turn overrides Smart and Scheduled DND.
return do_not_disturb_is_active();
}
static void prv_set_state(bool enabled, void *context) {
PBL_LOG(LOG_LEVEL_DEBUG, "Manual DND toggle: %s", enabled ? "enabled" : "disabled");
do_not_disturb_set_manually_enabled(enabled);
}
static const ActionToggleImpl s_dnd_action_toggle_impl = {
.window_name = "DNDManualToggle",
.prompt_icon = PBL_IF_RECT_ELSE(RESOURCE_ID_QUIET_TIME_MOUSE,
RESOURCE_ID_QUIET_TIME_MOUSE_RIGHT_ALIGNED),
.result_icon = RESOURCE_ID_QUIET_TIME_MOUSE,
.prompt_enable_message = i18n_noop("Start Quiet Time?"),
.prompt_disable_message = i18n_noop("End Quiet Time?"),
.result_enable_message = i18n_noop("Quiet Time\nStarted"),
.result_disable_message = i18n_noop("Quiet Time\nEnded"),
.callbacks = {
.get_state = prv_get_state,
.set_state = prv_set_state,
},
};
void do_not_disturb_toggle_push(ActionTogglePrompt prompt, bool set_exit_reason) {
action_toggle_push(&(ActionToggleConfig) {
.impl = &s_dnd_action_toggle_impl,
.prompt = prompt,
.set_exit_reason = set_exit_reason,
});
}

View file

@ -0,0 +1,21 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "applib/ui/action_toggle.h"
void do_not_disturb_toggle_push(ActionTogglePrompt prompt, bool set_exit_reason);

View file

@ -0,0 +1,28 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "applib/graphics/gtypes.h"
#define SMS_REPLY_COLOR GColorIslamicGreen
// Notif pref db key for send text
#define SEND_TEXT_NOTIF_PREF_KEY "com.pebble.sendText"
// Notif pref db keys for incoming call reply
#define ANDROID_PHONE_KEY "com.pebble.android.phone"
#define IOS_PHONE_KEY "com.apple.mobilephone"

View file

@ -0,0 +1,737 @@
/*
* 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 "notification_storage.h"
#include "notification_storage_private.h"
#include "util/uuid.h"
#include "kernel/pbl_malloc.h"
#include "services/normal/filesystem/pfs.h"
#include "services/common/system_task.h"
#include "system/logging.h"
#include "system/logging.h"
#include "os/mutex.h"
#include "system/passert.h"
#include "util/iterator.h"
#include <inttypes.h>
#include <stddef.h>
#include <string.h>
typedef struct NotificationIterState {
SerializedTimelineItemHeader header;
int fd;
TimelineItem notification;
} NotificationIterState;
static const char *FILENAME = "notifstr"; //The filename should not be changed
static PebbleRecursiveMutex *s_notif_storage_mutex = NULL;
static uint32_t s_write_offset;
static bool prv_iter_next(NotificationIterState *iter_state);
static bool prv_get_notification(TimelineItem *notification,
SerializedTimelineItemHeader *header, int fd);
static void prv_set_header_status(SerializedTimelineItemHeader *header, uint8_t status, int fd);
void notification_storage_init(void) {
PBL_ASSERTN(s_notif_storage_mutex == NULL);
//Clear notifications storage on reset
pfs_remove(FILENAME);
// Create a new file and close it (removes delay when receiving first notification after boot)
int fd = pfs_open(FILENAME, OP_FLAG_WRITE, FILE_TYPE_STATIC, NOTIFICATION_STORAGE_FILE_SIZE);
if (fd < 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Error opening file %d", fd);
} else {
pfs_close(fd);
}
s_write_offset = 0;
s_notif_storage_mutex = mutex_create_recursive();
}
void notification_storage_lock(void) {
mutex_lock_recursive(s_notif_storage_mutex);
}
void notification_storage_unlock(void) {
mutex_unlock_recursive(s_notif_storage_mutex);
}
static int prv_file_open(uint8_t op_flags) {
notification_storage_lock();
int fd = pfs_open(FILENAME, op_flags, FILE_TYPE_STATIC, NOTIFICATION_STORAGE_FILE_SIZE);
if (fd < 0) {
// If a write operation or a read operation fails with anything other than "does not exist",
// log error
bool read_only =
((op_flags & (OP_FLAG_WRITE | OP_FLAG_OVERWRITE | OP_FLAG_READ)) == OP_FLAG_READ);
if ((!read_only) || (fd != E_DOES_NOT_EXIST)) {
PBL_LOG(LOG_LEVEL_ERROR, "Error opening file %d", fd);
// Remove file so next open will create a new one (notification storage trashed)
pfs_remove(FILENAME);
}
notification_storage_unlock();
}
return fd;
}
static void prv_file_close(int fd) {
pfs_close(fd);
notification_storage_unlock();
}
static int prv_write_notification(TimelineItem *notification,
SerializedTimelineItemHeader *header, int fd) {
int bytes_written = 0;
// Invert flags & status to store on flash
header->common.flags = ~header->common.flags;
header->common.status = ~header->common.status;
int result = pfs_write(fd, (uint8_t *) header, sizeof(*header));
// Restore flags & status
header->common.flags = ~header->common.flags;
header->common.status = ~header->common.status;
if (result < 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Error writing notification header %d", result);
return result;
}
bytes_written += result;
if (!header->payload_length) {
return result;
}
uint8_t *write_buffer = kernel_malloc_check(header->payload_length);
timeline_item_serialize_payload(notification, write_buffer, header->payload_length);
result = pfs_write(fd, write_buffer, header->payload_length);
if (result < 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Error writing notification payload %d", result);
kernel_free(write_buffer);
return result;
}
bytes_written += result;
kernel_free(write_buffer);
return bytes_written;
}
//! Iterate over notifications space and mark the oldest notifications as deleted until we have
//! enough space available
static void prv_reclaim_space(size_t size_needed, int fd) {
size_needed = ((size_needed / NOTIFICATION_STORAGE_MINIMUM_INCREMENT_SIZE) + 1) *
NOTIFICATION_STORAGE_MINIMUM_INCREMENT_SIZE; // Free up space size in blocks
size_t size_available = 0;
NotificationIterState iter_state = {
.fd = fd,
};
Iterator iter;
iter_init(&iter, (IteratorCallback)&prv_iter_next, NULL, &iter_state);
while (iter_next(&iter)) {
uint8_t status = iter_state.header.common.status;
if (!(status & TimelineItemStatusDeleted)) {
// Mark for deletion
prv_set_header_status(&iter_state.header, TimelineItemStatusDeleted, fd);
size_available += sizeof(SerializedTimelineItemHeader) + iter_state.header.payload_length;
if (size_needed <= size_available) {
return;
}
}
if (pfs_seek(fd, iter_state.header.payload_length, FSeekCur) < 0) {
break;
}
}
}
//! Check whether there exists @ref size_needed available space in storage after compression
static bool prv_is_storage_full(size_t size_needed, size_t *size_available, int fd) {
*size_available = 0;
NotificationIterState iter_state = {
.fd = fd,
};
Iterator iter;
iter_init(&iter, (IteratorCallback)&prv_iter_next, NULL, &iter_state);
while (iter_next(&iter)) {
// Check header status to detect deleted notifications. Add size of deleted notifications
uint8_t status = iter_state.header.common.status;
if (status & TimelineItemStatusDeleted) {
*size_available += sizeof(SerializedTimelineItemHeader) + iter_state.header.payload_length;
if (size_needed <= *size_available) {
return false;
}
}
if (pfs_seek(fd, iter_state.header.payload_length, FSeekCur) < 0) {
break;
}
}
return true;
}
//! Compress storage by reading all valid notifications out of old file and storing to new file
//! via overwrite
static bool prv_compress(size_t size_needed, int *fd) {
pfs_seek(*fd, 0, FSeekSet);
//Open file for overwrite
int new_fd = pfs_open(FILENAME, OP_FLAG_OVERWRITE, FILE_TYPE_STATIC,
NOTIFICATION_STORAGE_FILE_SIZE);
if (new_fd < 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Error opening new file for compression %d", new_fd);
return false;
}
// Delete old notifications if there is no space left in storage
size_t size_available;
if (prv_is_storage_full(size_needed, &size_available, *fd)) {
pfs_seek(*fd, 0, FSeekSet);
prv_reclaim_space(size_needed - size_available, *fd);
}
pfs_seek(*fd, 0, FSeekSet);
int write_offset = 0;
// Iterate over notifications stored and write to new file
NotificationIterState iter_state = {
.fd = *fd,
};
Iterator iter;
iter_init(&iter, (IteratorCallback)&prv_iter_next, NULL, &iter_state);
while (iter_next(&iter)) {
// Check header flags to detect deleted notifications
uint8_t status = iter_state.header.common.status;
if (status & TimelineItemStatusDeleted) {
// Skip over deleted notification
pfs_seek(*fd, iter_state.header.payload_length, FSeekCur);
continue;
}
TimelineItem notification;
if (!prv_get_notification(&notification, &iter_state.header, *fd)) {
// Error occurred
goto cleanup;
}
int result = prv_write_notification(&notification, &iter_state.header, new_fd);
if (result < 0) {
// Error occurred
kernel_free(notification.allocated_buffer);
goto cleanup;
}
write_offset += result;
kernel_free(notification.allocated_buffer);
}
s_write_offset = write_offset;
pfs_close(*fd);
pfs_close(new_fd);
*fd = pfs_open(FILENAME, OP_FLAG_READ | OP_FLAG_WRITE, FILE_TYPE_STATIC,
NOTIFICATION_STORAGE_FILE_SIZE);
if (*fd < 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Error re-opening after compression %d", new_fd);
return false;
}
return true;
cleanup:
pfs_close(*fd);
pfs_close(new_fd);
return false;
}
void notification_storage_store(TimelineItem* notification) {
PBL_ASSERTN(s_notif_storage_mutex != NULL);
PBL_ASSERTN(notification != NULL);
SerializedTimelineItemHeader header = { .common.id = UUID_INVALID };
timeline_item_serialize_header(notification, &header);
int fd = prv_file_open(OP_FLAG_WRITE | OP_FLAG_READ);
if (fd < 0) {
return;
}
size_t size_needed = header.payload_length + sizeof(SerializedTimelineItemHeader);
if (size_needed > (NOTIFICATION_STORAGE_FILE_SIZE - s_write_offset)) {
if (!prv_compress(size_needed, &fd)) {
// Notification storage compression failed. Clear notifications storage
goto reset_storage;
}
}
pfs_seek(fd, s_write_offset, FSeekSet);
int result = prv_write_notification(notification, &header, fd);
if (result < 0) {
// [AS] TODO: Write failure: reset storage, compression or reset watch?
goto reset_storage;
}
s_write_offset += result;
prv_file_close(fd);
return;
reset_storage:
mutex_unlock_recursive(s_notif_storage_mutex);
notification_storage_reset_and_init();
}
// Finds the next match in the notification storage file from the current position
// Position in file will be at the start of notification payload if return value is true
static bool prv_find_next_notification(SerializedTimelineItemHeader* header,
bool (*compare_func)(SerializedTimelineItemHeader* header, void* data), void* data, int fd) {
for (;;) {
int result = pfs_read(fd, (uint8_t *)header, sizeof(*header));
// Restore flags & status
header->common.flags = ~header->common.flags;
header->common.status = ~header->common.status;
if ((result < 0) || (uuid_is_invalid(&header->common.id))) {
break;
}
uint8_t status = header->common.status;
if ((status & TimelineItemStatusUnused) ||
(header->common.type >= TimelineItemTypeOutOfRange) ||
(header->common.layout >= NumLayoutIds)) {
pfs_close(fd);
notification_storage_reset_and_init();
PBL_LOG(LOG_LEVEL_ERROR, "Notification storage corrupt. Resetting...");
break;
}
if (!(status & TimelineItemStatusDeleted)) {
// Only check compare notifications if it is not deleted, otherwise skip it and look for
// the next match
if (compare_func(header, data)) {
return true;
}
}
if (pfs_seek(fd, header->payload_length, FSeekCur) < 0) {
break;
}
}
return false;
}
static bool prv_uuid_equal_func(SerializedTimelineItemHeader *header, void *data) {
Uuid *uuid = (Uuid *)data;
return uuid_equal(&header->common.id, uuid);
}
static bool prv_ancs_id_compare_func(SerializedTimelineItemHeader* header, void* data) {
uint32_t ancs_uid = (uint32_t) data;
return header->common.ancs_uid == ancs_uid;
}
bool notification_storage_notification_exists(const Uuid *id) {
int fd = prv_file_open(OP_FLAG_READ);
if (fd < 0) {
return false;
}
SerializedTimelineItemHeader header = { .common.id = UUID_INVALID };
bool found = prv_find_next_notification(&header, prv_uuid_equal_func, (void *)id, fd);
prv_file_close(fd);
return found;
}
static bool prv_get_notification(TimelineItem *notification,
SerializedTimelineItemHeader* header, int fd) {
notification->allocated_buffer = NULL; // Must be initialized in case this goes to cleanup
// Read notification to temporary buffer
uint8_t *read_buffer = task_zalloc_check(header->payload_length);
int result = pfs_read(fd, read_buffer, header->payload_length);
if (result < 0) {
goto cleanup;
}
if (!timeline_item_deserialize_item(notification, header, read_buffer)) {
goto cleanup;
}
task_free(read_buffer);
return true;
cleanup:
task_free(read_buffer);
return false;
}
size_t notification_storage_get_len(const Uuid *uuid) {
int fd = prv_file_open(OP_FLAG_READ);
if (fd < 0) {
return 0;
}
size_t size = 0;
SerializedTimelineItemHeader header = { .common.id = UUID_INVALID };
if (prv_find_next_notification(&header, prv_uuid_equal_func, (void *) uuid, fd)) {
size = header.payload_length + sizeof(SerializedTimelineItemHeader);
} else {
PBL_LOG(LOG_LEVEL_DEBUG, "notification not found");
}
prv_file_close(fd);
return (size);
}
bool notification_storage_get(const Uuid *id, TimelineItem *item_out) {
PBL_ASSERTN(item_out && (s_notif_storage_mutex != NULL));
int fd = prv_file_open(OP_FLAG_READ);
if (fd < 0) {
return false;
}
bool rv = true;
SerializedTimelineItemHeader header = { .common.id = UUID_INVALID };
char uuid_string[UUID_STRING_BUFFER_LENGTH];
uuid_to_string(id, uuid_string);
if (!prv_find_next_notification(&header, prv_uuid_equal_func, (void *) id, fd)) {
PBL_LOG(LOG_LEVEL_DEBUG, "notification not found, %s", uuid_string);
rv = false;
} else {
if (!prv_get_notification(item_out, &header, fd)) {
PBL_LOG(LOG_LEVEL_ERROR, "Could not retrieve notification with id %s and size %u",
uuid_string, header.payload_length);
rv = false;
}
}
prv_file_close(fd);
return rv;
}
//! @return is_advanced
static bool prv_iter_next(NotificationIterState *iter_state) {
int result = pfs_read(iter_state->fd, (uint8_t *)&iter_state->header, sizeof(iter_state->header));
// Restore flags & status
iter_state->header.common.flags = ~iter_state->header.common.flags;
iter_state->header.common.status = ~iter_state->header.common.status;
if ((result == E_RANGE) || (uuid_is_invalid(&iter_state->header.common.id))) {
//End iteration if we have reached the end of the file or the header ID is invalid (erased flash)
return false;
} else if (result < 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Error reading notification header while iterating %d", result);
return false;
}
return true;
}
static bool prv_rewrite_iter_next(NotificationIterState *iter_state) {
int result = 0;
result = pfs_read(iter_state->fd, (uint8_t*)&iter_state->header, sizeof(iter_state->header));
// Restore flags & status
iter_state->header.common.flags = ~iter_state->header.common.flags;
iter_state->header.common.status = ~iter_state->header.common.status;
if ((result == E_RANGE) || (uuid_is_invalid(&iter_state->header.common.id))) {
return false;
} else if (result < 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Error reading notification header while iterating %d", result);
return false;
}
if (iter_state->header.common.status & TimelineItemStatusDeleted) {
return true;
}
return prv_get_notification(&iter_state->notification, &iter_state->header, iter_state->fd);
}
static void prv_set_header_status(SerializedTimelineItemHeader *header, uint8_t status, int fd) {
// Seek to the status field
pfs_seek(fd, (-(int)sizeof(*header) +
(int)offsetof(CommonTimelineItemHeader, status)),
FSeekCur);
// Invert flags & status to store on flash
status = ~status;
int result = pfs_write(fd, (uint8_t *)&status, sizeof(header->common.status));
if (result < 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Error writing status to notification header %d", result);
}
// Seek to the end of the header
pfs_seek(fd, ((int)sizeof(*header) - (int)offsetof(CommonTimelineItemHeader, status) -
sizeof(header->common.status)), FSeekCur);
}
bool notification_storage_get_status(const Uuid *id, uint8_t *status) {
int fd = prv_file_open(OP_FLAG_READ);
bool rv = false;
if (fd < 0) {
return rv;
}
SerializedTimelineItemHeader header = { .common.id = UUID_INVALID };
if (prv_find_next_notification(&header, prv_uuid_equal_func, (void *) id, fd)) {
*status = header.common.status;
rv = true;
}
prv_file_close(fd);
return rv;
}
void notification_storage_set_status(const Uuid *id, uint8_t status) {
PBL_ASSERTN(s_notif_storage_mutex != NULL);
SerializedTimelineItemHeader header = { .common.id = UUID_INVALID };
int fd = prv_file_open(OP_FLAG_READ | OP_FLAG_WRITE);
if (fd < 0) {
return;
}
if (prv_find_next_notification(&header, prv_uuid_equal_func, (void *) id, fd)) {
prv_set_header_status(&header, status, fd);
}
prv_file_close(fd);
}
void notification_storage_remove(const Uuid *id) {
notification_storage_set_status(id, TimelineItemStatusDeleted);
}
bool notification_storage_find_ancs_notification_id(uint32_t ancs_uid, Uuid *uuid_out) {
PBL_ASSERTN(s_notif_storage_mutex != NULL);
int fd = prv_file_open(OP_FLAG_READ);
if (fd < 0) {
return false;
}
SerializedTimelineItemHeader header = { .common.id = UUID_INVALID };
// Find the most recent notification which matches this ANCS UID - this will be the last entry in
// the db. iOS can reset ANCS UIDs on reconnect, so we want to avoid finding an old notification
bool found = false;
while (prv_find_next_notification(&header, prv_ancs_id_compare_func,
(void *)(uintptr_t) ancs_uid, fd)) {
found = true;
*uuid_out = header.common.id;
// Seek to the end of this item's payload (start of the next item)
if (pfs_seek(fd, header.payload_length, FSeekCur) < 0) {
break;
}
}
prv_file_close(fd);
return found;
}
static bool prv_compare_ancs_notifications(TimelineItem *notification, const uint8_t *payload,
size_t payload_size, SerializedTimelineItemHeader *header, int fd) {
if ((notification->header.timestamp != header->common.timestamp) ||
(notification->header.layout != header->common.layout) ||
(header->payload_length != payload_size)) {
return false;
}
bool found = false;
if (header->payload_length == payload_size) {
uint8_t *read_buffer = kernel_malloc_check(payload_size);
int result = pfs_read(fd, read_buffer, payload_size);
if (result < 0) {
kernel_free(read_buffer);
return false;
}
//Seek back to the end of the header so that the next iterator seek finds the next record
pfs_seek(fd, -payload_size, FSeekCur);
found = (memcmp(payload, read_buffer, payload_size) == 0);
kernel_free(read_buffer);
}
return found;
}
bool notification_storage_find_ancs_notification_by_timestamp(
TimelineItem *notification, CommonTimelineItemHeader *header_out) {
PBL_ASSERTN(s_notif_storage_mutex && notification && header_out);
int fd = prv_file_open(OP_FLAG_READ);
if (fd < 0) {
return false;
}
//Serialize notification attributes and actions for easy comparison
size_t payload_size = timeline_item_get_serialized_payload_size(notification);
uint8_t *payload = kernel_malloc_check(payload_size);
timeline_item_serialize_payload(notification, payload, payload_size);
//Iterate over all records until a match is found
bool rv = false;
NotificationIterState iter_state = {
.fd = fd,
};
Iterator iter;
iter_init(&iter, (IteratorCallback)&prv_iter_next, NULL, &iter_state);
while (iter_next(&iter)) {
// Check header flags to detect deleted notifications
uint8_t status = iter_state.header.common.status;
if (!(status & TimelineItemStatusDeleted)) {
if (prv_compare_ancs_notifications(notification, payload, payload_size, &iter_state.header,
fd)) {
*header_out = iter_state.header.common;
rv = true;
break;
}
}
int result = pfs_seek(iter_state.fd, iter_state.header.payload_length, FSeekCur);
if (result < 0) {
break;
}
}
kernel_free(payload);
prv_file_close(fd);
return rv;
}
void notification_storage_rewrite(void (*iter_callback)(TimelineItem *notification,
SerializedTimelineItemHeader *header, void *data), void *data) {
PBL_ASSERTN(s_notif_storage_mutex != NULL);
if (iter_callback == NULL) {
return;
}
int fd = prv_file_open(OP_FLAG_READ | OP_FLAG_WRITE);
if (fd < 0) {
return;
}
int new_fd = pfs_open(FILENAME, OP_FLAG_OVERWRITE | OP_FLAG_READ, FILE_TYPE_STATIC,
NOTIFICATION_STORAGE_FILE_SIZE);
if (new_fd < 0) {
prv_file_close(fd);
return;
}
Iterator iter;
NotificationIterState iter_state = {
.fd = fd
};
iter_init(&iter, (IteratorCallback)prv_rewrite_iter_next, NULL, &iter_state);
while (iter_next(&iter)) {
uint8_t status = iter_state.header.common.status;
if (!(status & TimelineItemStatusDeleted)) {
iter_callback(&iter_state.notification, &iter_state.header, data);
}
prv_write_notification(&iter_state.notification, &iter_state.header, new_fd);
}
// Close the old file
prv_file_close(fd);
// We have to close and reopend the new file pointed to by the file descriptor, so
// that it's temp flag is cleared.
pfs_close(new_fd);
new_fd = prv_file_open(OP_FLAG_READ | OP_FLAG_WRITE);
// Finally, close that new file as this fd is only known here
prv_file_close(new_fd);
}
void notification_storage_iterate(bool (*iter_callback)(void *data,
SerializedTimelineItemHeader *header), void *data) {
PBL_ASSERTN(s_notif_storage_mutex != NULL);
if (iter_callback == NULL) {
return;
}
int fd = prv_file_open(OP_FLAG_READ);
if (fd < 0) {
return;
}
Iterator iter;
NotificationIterState iter_state = {
.fd = fd
};
iter_init(&iter, (IteratorCallback)prv_iter_next, NULL, &iter_state);
while (iter_next(&iter)) {
uint8_t status = iter_state.header.common.status;
if (!(status & TimelineItemStatusDeleted)) {
if (!iter_callback(data, &iter_state.header)) {
break;
}
}
int result = pfs_seek(iter_state.fd, iter_state.header.payload_length, FSeekCur);
if (result < 0) {
break;
}
}
prv_file_close(fd);
}
void notification_storage_reset_and_init(void) {
notification_storage_lock();
pfs_remove(FILENAME);
s_write_offset = 0;
notification_storage_unlock();
}
// Added for use by unit tests. Do not call from firmware
#if UNITTEST
void notification_storage_reset(void) {
if (s_notif_storage_mutex != NULL) {
mutex_destroy((PebbleMutex *) s_notif_storage_mutex);
s_notif_storage_mutex = NULL;
}
notification_storage_init();
}
#endif

View file

@ -0,0 +1,86 @@
/*
* 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 "kernel/events.h"
#include "util/iterator.h"
void notification_storage_init(void);
// Use notification_storage_lock and notification_storage_unlock to perform multiple
// actions on the storage that should not be interrupted
//! Recursively lock storage mutex
void notification_storage_lock(void);
//! Recursively unlock storage mutex
void notification_storage_unlock(void);
//! Store a notification to flash
void notification_storage_store(TimelineItem* notification);
//! Check if a notification exists in storage
bool notification_storage_notification_exists(const Uuid *id);
size_t notification_storage_get_len(const Uuid *uuid);
//! Get a notification from flash. The allocated_buffer of the returned notification must be freed
//! When no longer in use
bool notification_storage_get(const Uuid *id, TimelineItem *item_out);
//! Set the status of a stored notification
void notification_storage_set_status(const Uuid *id, uint8_t status);
//! Get the status for a stored notification, returns false if not found
bool notification_storage_get_status(const Uuid *id, uint8_t *status);
//! Remove a notification from storage (mark it for deletion)
void notification_storage_remove(const Uuid *id);
//! Find a notification in storage with a matching ANCS UID
bool notification_storage_find_ancs_notification_id(uint32_t ancs_uid, Uuid *uuid_out);
//! Finds a notification that is identical to the specified one by first searching the timestamps
//! and then comparing the actions and attributes
//! @param notification Notification to match with
//! @param header_out Header of matching notification
//! @return true if matching notification found, else false
bool notification_storage_find_ancs_notification_by_timestamp(
TimelineItem *notification, CommonTimelineItemHeader *header_out);
//! Iterates over all of the notifications in the storage, calling the iterator callback with
//! the header ID of each one
//! NOTE: Do NOT call into other notification storage functions from the iterator callback. It will
//! cause corruption of notification storage
void notification_storage_iterate(bool (*iter_callback)(void *data,
SerializedTimelineItemHeader *header_id), void *data);
//! Iterates over all the notifications calling the callback with the passed data.
//! Overwrites the notifications and rewrites them to disk.
//! This is essentially a noop if the callback doesn't alter the data.
void notification_storage_rewrite(void (*iter_callback)(TimelineItem *notification,
SerializedTimelineItemHeader *header, void *data), void *data);
//! Clear out all notifications and reset all state immediately.
void notification_storage_reset_and_init(void);
#if UNITTEST
//! Clear out all notifications and reset all state. Used for unit testing.
void notification_storage_reset(void);
#endif

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
//! Notification storage file size
#define NOTIFICATION_STORAGE_FILE_SIZE (30 * 1024)
//! Minimum increment of space to free up when compressing.
//! The higher the value, the less often we need to compress,
//! but we will lose more notifications
#define NOTIFICATION_STORAGE_MINIMUM_INCREMENT_SIZE (NOTIFICATION_STORAGE_FILE_SIZE / 4)

View file

@ -0,0 +1,34 @@
/*
* 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"
//! This list is shared by notifications and reminders.
typedef enum {
NotificationInvalid = 0,
NotificationMobile = (1 << 0),
NotificationPhoneCall = (1 << 1),
NotificationOther = (1 << 2),
NotificationReminder = (1 << 3)
} NotificationType;
//! Type and Id for the notification or reminder.
typedef struct {
NotificationType type;
Uuid id;
} NotificationInfo;

View file

@ -0,0 +1,117 @@
/*
* 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.h"
#include "notification_storage.h"
#include "do_not_disturb.h"
#include "applib/ui/vibes.h"
#include "drivers/rtc.h"
#include "drivers/battery.h"
#include "resource/resource_ids.auto.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/bitset.h"
#include "kernel/events.h"
#include "kernel/low_power.h"
#include "kernel/pbl_malloc.h"
#include "services/common/analytics/analytics.h"
#include "services/common/evented_timer.h"
#include "services/common/i18n/i18n.h"
#include "services/normal/blob_db/reminder_db.h"
#include "services/normal/phone_call.h"
#include "services/normal/timeline/attribute.h"
#include "services/normal/timeline/timeline.h"
#include "services/normal/vibes/vibe_intensity.h"
#include <string.h>
static void prv_notification_migration_iterator_callback(TimelineItem *notification,
SerializedTimelineItemHeader *header, void *data) {
header->common.timestamp -= *((int*)data);
notification->header.timestamp = header->common.timestamp;
}
void notifications_handle_notification_action_result(
PebbleSysNotificationActionResult *action_result) {
PebbleEvent launcher_event = {
.type = PEBBLE_SYS_NOTIFICATION_EVENT,
.sys_notification = {
.type = NotificationActionResult,
.action_result = action_result,
}
};
// event loop will free memory of action_result
event_put(&launcher_event);
}
void notifications_handle_notification_removed(Uuid *notification_id) {
Uuid *removed_id = kernel_malloc_check(sizeof(Uuid));
*removed_id = *notification_id;
PebbleEvent launcher_event = {
.type = PEBBLE_SYS_NOTIFICATION_EVENT,
.sys_notification = {
.type = NotificationRemoved,
.notification_id = removed_id,
}
};
event_put(&launcher_event);
}
void notifications_handle_notification_added(Uuid *notification_id) {
PebbleEvent launcher_event = {
.type = PEBBLE_SYS_NOTIFICATION_EVENT,
.sys_notification = {
.type = NotificationAdded,
.notification_id = notification_id
}
};
event_put(&launcher_event);
analytics_inc(ANALYTICS_DEVICE_METRIC_NOTIFICATION_RECEIVED_COUNT, AnalyticsClient_System);
}
void notifications_handle_notification_acted_upon(Uuid *notification_id) {
PebbleEvent launcher_event = {
.type = PEBBLE_SYS_NOTIFICATION_EVENT,
.sys_notification = {
.type = NotificationActedUpon,
.notification_id = notification_id
}
};
event_put(&launcher_event);
}
void notifications_migrate_timezone(const int tz_diff) {
notification_storage_rewrite(prv_notification_migration_iterator_callback, (void*)&tz_diff);
}
void notification_storage_init(void);
void vibe_intensity_init(void);
void notifications_init(void) {
notification_storage_init();
}
void notifications_add_notification(TimelineItem *notification) {
notification_storage_store(notification);
Uuid *uuid = kernel_malloc_check(sizeof(Uuid));
*uuid = notification->header.id;
notifications_handle_notification_added(uuid);
}

View file

@ -0,0 +1,64 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "services/normal/timeline/item.h"
#include <stdbool.h>
#include <stdint.h>
typedef enum {
ActionResultTypeSuccess,
ActionResultTypeFailure,
ActionResultTypeChaining,
ActionResultTypeDoResponse,
ActionResultTypeSuccessANCSDismiss,
} ActionResultType;
typedef struct {
Uuid id;
ActionResultType type;
AttributeList attr_list;
TimelineItemActionGroup action_group;
} PebbleSysNotificationActionResult;
void notifications_init(void);
//! Feedback for the result of an invoke action command
void notifications_handle_notification_action_result(
PebbleSysNotificationActionResult *action_result);
//! Add a notification.
void notifications_handle_notification_added(Uuid *notification_id);
//! Handle a notification getting acted upon on the phone
void notifications_handle_notification_acted_upon(Uuid *notification_id);
//! Remove a notification
void notifications_handle_notification_removed(Uuid *notification_id);
//! Notify of remove command from ANCS. Notification will be kept in history
void notifications_handle_ancs_notification_removed(uint32_t ancs_uid);
//! Migration hook for notifications
//! Called with the GMT offset of the new timezone
void notifications_migrate_timezone(const int new_tz_offset);
//! Inserts a new notification into notification storage and notifies the system of the new item
//! @param notification Pointer to the notification to add
void notifications_add_notification(TimelineItem *notification);