mirror of
https://github.com/google/pebble.git
synced 2025-07-21 06:44:49 -04:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
173
src/fw/services/normal/notifications/action_chaining_window.c
Normal file
173
src/fw/services/normal/notifications/action_chaining_window.c
Normal 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);
|
||||
}
|
|
@ -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);
|
133
src/fw/services/normal/notifications/alerts.c
Normal file
133
src/fw/services/normal/notifications/alerts.c
Normal 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();
|
||||
}
|
45
src/fw/services/normal/notifications/alerts.h
Normal file
45
src/fw/services/normal/notifications/alerts.h
Normal 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();
|
475
src/fw/services/normal/notifications/alerts_preferences.c
Normal file
475
src/fw/services/normal/notifications/alerts_preferences.c
Normal 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);
|
||||
}
|
38
src/fw/services/normal/notifications/alerts_preferences.h
Normal file
38
src/fw/services/normal/notifications/alerts_preferences.h
Normal 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);
|
|
@ -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);
|
||||
|
47
src/fw/services/normal/notifications/alerts_private.h
Normal file
47
src/fw/services/normal/notifications/alerts_private.h
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "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);
|
145
src/fw/services/normal/notifications/ancs/ancs_filtering.c
Normal file
145
src/fw/services/normal/notifications/ancs/ancs_filtering.c
Normal 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);
|
||||
}
|
42
src/fw/services/normal/notifications/ancs/ancs_filtering.h
Normal file
42
src/fw/services/normal/notifications/ancs/ancs_filtering.h
Normal 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);
|
485
src/fw/services/normal/notifications/ancs/ancs_item.c
Normal file
485
src/fw/services/normal/notifications/ancs/ancs_item.c
Normal 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 = ¬if_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 = ¬if_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 ? ¬if_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);
|
||||
}
|
||||
}
|
46
src/fw/services/normal/notifications/ancs/ancs_item.h
Normal file
46
src/fw/services/normal/notifications/ancs/ancs_item.h
Normal 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);
|
70
src/fw/services/normal/notifications/ancs/ancs_known_apps.h
Normal file
70
src/fw/services/normal/notifications/ancs/ancs_known_apps.h
Normal 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
|
391
src/fw/services/normal/notifications/ancs/ancs_notifications.c
Normal file
391
src/fw/services/normal/notifications/ancs/ancs_notifications.c
Normal 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(¬ification->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(¬ification->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(¬ification->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);
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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(×tamp, 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;
|
||||
}
|
|
@ -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);
|
108
src/fw/services/normal/notifications/ancs/ancs_phone_call.c
Normal file
108
src/fw/services/normal/notifications/ancs/ancs_phone_call.c
Normal 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);
|
||||
}
|
39
src/fw/services/normal/notifications/ancs/ancs_phone_call.h
Normal file
39
src/fw/services/normal/notifications/ancs/ancs_phone_call.h
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "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);
|
65
src/fw/services/normal/notifications/ancs/nexmo.c
Normal file
65
src/fw/services/normal/notifications/ancs/nexmo.c
Normal 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);
|
||||
}
|
28
src/fw/services/normal/notifications/ancs/nexmo.h
Normal file
28
src/fw/services/normal/notifications/ancs/nexmo.h
Normal 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);
|
354
src/fw/services/normal/notifications/do_not_disturb.c
Normal file
354
src/fw/services/normal/notifications/do_not_disturb.c
Normal 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
|
100
src/fw/services/normal/notifications/do_not_disturb.h
Normal file
100
src/fw/services/normal/notifications/do_not_disturb.h
Normal 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
|
57
src/fw/services/normal/notifications/do_not_disturb_toggle.c
Normal file
57
src/fw/services/normal/notifications/do_not_disturb_toggle.c
Normal 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,
|
||||
});
|
||||
}
|
21
src/fw/services/normal/notifications/do_not_disturb_toggle.h
Normal file
21
src/fw/services/normal/notifications/do_not_disturb_toggle.h
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/ui/action_toggle.h"
|
||||
|
||||
void do_not_disturb_toggle_push(ActionTogglePrompt prompt, bool set_exit_reason);
|
|
@ -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"
|
737
src/fw/services/normal/notifications/notification_storage.c
Normal file
737
src/fw/services/normal/notifications/notification_storage.c
Normal 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(¬ification, &iter_state.header, *fd)) {
|
||||
// Error occurred
|
||||
goto cleanup;
|
||||
}
|
||||
int result = prv_write_notification(¬ification, &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
|
86
src/fw/services/normal/notifications/notification_storage.h
Normal file
86
src/fw/services/normal/notifications/notification_storage.h
Normal 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
|
|
@ -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)
|
||||
|
34
src/fw/services/normal/notifications/notification_types.h
Normal file
34
src/fw/services/normal/notifications/notification_types.h
Normal 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;
|
117
src/fw/services/normal/notifications/notifications.c
Normal file
117
src/fw/services/normal/notifications/notifications.c
Normal 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);
|
||||
}
|
64
src/fw/services/normal/notifications/notifications.h
Normal file
64
src/fw/services/normal/notifications/notifications.h
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "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);
|
Loading…
Add table
Add a link
Reference in a new issue