mirror of
https://github.com/google/pebble.git
synced 2025-05-22 03:14:52 +00:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
400
src/fw/apps/system_apps/send_text/send_text.c
Normal file
400
src/fw/apps/system_apps/send_text/send_text.c
Normal file
|
@ -0,0 +1,400 @@
|
|||
/*
|
||||
* 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 "send_text.h"
|
||||
#include "send_text_app_prefs.h"
|
||||
|
||||
#include "applib/app.h"
|
||||
#include "applib/app_exit_reason.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/ui.h"
|
||||
#include "apps/system_apps/timeline/peek_layer.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "services/common/i18n/i18n.h"
|
||||
#include "services/normal/blob_db/contacts_db.h"
|
||||
#include "services/normal/blob_db/ios_notif_pref_db.h"
|
||||
#include "services/normal/blob_db/watch_app_prefs_db.h"
|
||||
#include "services/normal/contacts/contacts.h"
|
||||
#include "services/normal/notifications/notification_constants.h"
|
||||
#include "services/normal/send_text_service.h"
|
||||
#include "services/normal/timeline/timeline.h"
|
||||
#include "services/normal/timeline/timeline_actions.h"
|
||||
#include "shell/prefs.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/time/time.h"
|
||||
#include "util/trig.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define SEND_TEXT_APP_HIGHLIGHT_COLOR PBL_IF_COLOR_ELSE(SMS_REPLY_COLOR, GColorBlack)
|
||||
|
||||
typedef int ContactId;
|
||||
|
||||
typedef struct {
|
||||
ListNode node;
|
||||
ContactId id;
|
||||
char *name;
|
||||
char *display_number;
|
||||
char *number; // Points to a substring within display_number (the part without the ❤)
|
||||
} ContactNode;
|
||||
|
||||
typedef struct SendTextAppData {
|
||||
Window window;
|
||||
MenuLayer menu_layer;
|
||||
PeekLayer no_contacts_layer;
|
||||
StatusBarLayer status_layer;
|
||||
|
||||
ContactNode *contact_list_head;
|
||||
|
||||
EventServiceInfo event_service_info;
|
||||
} SendTextAppData;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//! Action menu functions
|
||||
|
||||
static void prv_action_menu_did_close(ActionMenu *action_menu, const ActionMenuItem *item,
|
||||
void *context) {
|
||||
TimelineItem *timeline_item = context;
|
||||
timeline_item_destroy(timeline_item);
|
||||
}
|
||||
|
||||
static void prv_action_handle_response(PebbleEvent *e, void *context) {
|
||||
SendTextAppData *data = context;
|
||||
|
||||
if (e->sys_notification.type != NotificationActionResult) {
|
||||
// Not what we want
|
||||
return;
|
||||
}
|
||||
|
||||
PebbleSysNotificationActionResult *action_result = e->sys_notification.action_result;
|
||||
if (action_result == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Each action result can only service one response event
|
||||
event_service_client_unsubscribe(&data->event_service_info);
|
||||
|
||||
if (action_result->type == ActionResultTypeSuccess) {
|
||||
// Set the exit reason as "action performed successfully" so we return to the watchface
|
||||
// when we remove the window from the stack to exit the app
|
||||
app_exit_reason_set(APP_EXIT_ACTION_PERFORMED_SUCCESSFULLY);
|
||||
app_window_stack_remove(&data->window, false);
|
||||
}
|
||||
}
|
||||
|
||||
static TimelineItem *prv_create_timeline_item(const char *number) {
|
||||
iOSNotifPrefs *notif_prefs = ios_notif_pref_db_get_prefs((uint8_t *)SEND_TEXT_NOTIF_PREF_KEY,
|
||||
strlen(SEND_TEXT_NOTIF_PREF_KEY));
|
||||
if (!notif_prefs) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
AttributeList attr_list = {};
|
||||
attribute_list_add_cstring(&attr_list, AttributeIdSender, number);
|
||||
|
||||
TimelineItem *item = timeline_item_create_with_attributes(0, 0,
|
||||
TimelineItemTypeNotification,
|
||||
LayoutIdNotification,
|
||||
&attr_list,
|
||||
¬if_prefs->action_group);
|
||||
if (item) {
|
||||
item->header.id = (Uuid)UUID_SEND_SMS;
|
||||
item->header.parent_id = (Uuid)UUID_SEND_TEXT_DATA_SOURCE;
|
||||
}
|
||||
|
||||
attribute_list_destroy_list(&attr_list);
|
||||
ios_notif_pref_db_free_prefs(notif_prefs);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
static void prv_open_action_menu(SendTextAppData *data, const char *number) {
|
||||
TimelineItem *item = prv_create_timeline_item(number);
|
||||
|
||||
// This handles the case where item is NULL, so no need to check for that
|
||||
TimelineItemAction *reply_action = timeline_item_find_action_by_type(
|
||||
item, TimelineItemActionTypeResponse);
|
||||
|
||||
if (!reply_action) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Not opening response menu - unable to load reply action");
|
||||
timeline_item_destroy(item);
|
||||
return;
|
||||
}
|
||||
|
||||
timeline_actions_push_response_menu(item, reply_action, SEND_TEXT_APP_HIGHLIGHT_COLOR,
|
||||
prv_action_menu_did_close, data->window.parent_window_stack,
|
||||
TimelineItemActionSourceSendTextApp,
|
||||
false /* standalone_reply */);
|
||||
|
||||
data->event_service_info = (EventServiceInfo) {
|
||||
.type = PEBBLE_SYS_NOTIFICATION_EVENT,
|
||||
.handler = prv_action_handle_response,
|
||||
.context = data,
|
||||
};
|
||||
event_service_client_subscribe(&data->event_service_info);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//! Contact list functions
|
||||
|
||||
static void prv_clear_contact_list(SendTextAppData *data) {
|
||||
while (data->contact_list_head) {
|
||||
ContactNode *old_head = data->contact_list_head;
|
||||
data->contact_list_head = (ContactNode *)list_pop_head((ListNode *)old_head);
|
||||
|
||||
// Only need to free display_number since number shares the same buffer
|
||||
app_free(old_head->display_number);
|
||||
app_free(old_head->name);
|
||||
app_free(old_head);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_add_contact_to_list(ContactId id, const char *name, const char *number, bool is_fav,
|
||||
void *callback_context) {
|
||||
SendTextAppData *data = callback_context;
|
||||
|
||||
ContactNode *new_node = app_zalloc_check(sizeof(ContactNode));
|
||||
list_init((ListNode *)new_node);
|
||||
new_node->id = id;
|
||||
|
||||
const size_t name_size = strlen(name) + 1;
|
||||
new_node->name = app_zalloc_check(name_size);
|
||||
strcpy(new_node->name, name);
|
||||
|
||||
const char *fav_str = (is_fav ? "❤ " : "");
|
||||
const size_t display_number_size = strlen(fav_str) + strlen(number) + 1;
|
||||
new_node->display_number = app_zalloc_check(display_number_size);
|
||||
strcpy(new_node->display_number, fav_str);
|
||||
strcat(new_node->display_number, number);
|
||||
|
||||
// Store the number string (the part after "fav_str") to forward to the phone
|
||||
new_node->number = (new_node->display_number + strlen(fav_str));
|
||||
|
||||
list_append((ListNode *)data->contact_list_head, (ListNode *)new_node);
|
||||
|
||||
if (!data->contact_list_head) {
|
||||
data->contact_list_head = new_node;
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_read_contacts_from_prefs(SendTextAppData *data) {
|
||||
SerializedSendTextPrefs *prefs = watch_app_prefs_get_send_text();
|
||||
if (prefs) {
|
||||
int num_contacts = 0;
|
||||
|
||||
for (int i = 0; i < prefs->num_contacts; i++) {
|
||||
SerializedSendTextContact *pref = &prefs->contacts[i];
|
||||
|
||||
Contact *contact = contacts_get_contact_by_uuid(&pref->contact_uuid);
|
||||
if (!contact) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = 0; j < contact->addr_list.num_addresses; j++) {
|
||||
if (uuid_equal(&contact->addr_list.addresses[j].id, &pref->address_uuid)) {
|
||||
const char *name = attribute_get_string(&contact->attr_list, AttributeIdTitle,
|
||||
(char *)i18n_get("Unknown", data));
|
||||
const char *number = attribute_get_string(&contact->addr_list.addresses[j].attr_list,
|
||||
AttributeIdAddress, "");
|
||||
prv_add_contact_to_list(num_contacts++, name, number, pref->is_fav, data);
|
||||
}
|
||||
}
|
||||
|
||||
contacts_free_contact(contact);
|
||||
}
|
||||
}
|
||||
|
||||
task_free(prefs);
|
||||
}
|
||||
|
||||
static void prv_update_contact_list(SendTextAppData *data) {
|
||||
prv_clear_contact_list(data);
|
||||
prv_read_contacts_from_prefs(data);
|
||||
}
|
||||
|
||||
static bool prv_has_contacts(SendTextAppData *data) {
|
||||
return (list_count((ListNode *)data->contact_list_head) > 0);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//! Menu Layer Callbacks
|
||||
|
||||
static uint16_t prv_contact_list_get_num_rows_callback(MenuLayer *menu_layer,
|
||||
uint16_t section_index,
|
||||
void *callback_context) {
|
||||
SendTextAppData *data = callback_context;
|
||||
return (uint16_t)list_count((ListNode *)data->contact_list_head);
|
||||
}
|
||||
|
||||
static int16_t prv_contact_list_get_header_height_callback(MenuLayer *menu_layer,
|
||||
uint16_t section_index,
|
||||
void *callback_context) {
|
||||
return MENU_CELL_ROUND_UNFOCUSED_SHORT_CELL_HEIGHT;
|
||||
}
|
||||
|
||||
static int16_t prv_contact_list_get_cell_height_callback(MenuLayer *menu_layer,
|
||||
MenuIndex *cell_index,
|
||||
void *callback_context) {
|
||||
return PBL_IF_RECT_ELSE(menu_cell_basic_cell_height(),
|
||||
(menu_layer_is_index_selected(menu_layer, cell_index) ?
|
||||
MENU_CELL_ROUND_FOCUSED_SHORT_CELL_HEIGHT :
|
||||
MENU_CELL_ROUND_UNFOCUSED_TALL_CELL_HEIGHT));
|
||||
}
|
||||
|
||||
static void prv_contact_list_draw_header_callback(GContext *ctx, const Layer *cell_layer,
|
||||
uint16_t section_index, void *callback_context) {
|
||||
SendTextAppData *data = callback_context;
|
||||
const MenuIndex menu_index = menu_layer_get_selected_index(&data->menu_layer);
|
||||
const GFont font = fonts_get_system_font(FONT_KEY_GOTHIC_14);
|
||||
GRect box = cell_layer->bounds;
|
||||
box.origin.y -= 2;
|
||||
graphics_context_set_text_color(ctx, GColorDarkGray);
|
||||
graphics_draw_text(ctx, i18n_get("Select Contact", data), font, box, GTextOverflowModeFill,
|
||||
GTextAlignmentCenter, NULL);
|
||||
}
|
||||
|
||||
static void prv_contact_list_draw_row_callback(GContext *ctx, const Layer *cell_layer,
|
||||
MenuIndex *cell_index, void *callback_context) {
|
||||
SendTextAppData *data = (SendTextAppData *)callback_context;
|
||||
|
||||
ContactNode *node = (ContactNode *)list_get_at((ListNode *)data->contact_list_head,
|
||||
cell_index->row);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu_cell_basic_draw(ctx, cell_layer, node->name, node->display_number, NULL);
|
||||
}
|
||||
|
||||
static void prv_contact_list_select_callback(MenuLayer *menu_layer, MenuIndex *cell_index,
|
||||
void *callback_context) {
|
||||
SendTextAppData *data = (SendTextAppData *)callback_context;
|
||||
|
||||
ContactNode *node = (ContactNode *)list_get_at((ListNode *)data->contact_list_head,
|
||||
cell_index->row);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
prv_open_action_menu(data, node->number);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//! App boilerplate
|
||||
|
||||
static void prv_init(void) {
|
||||
SendTextAppData *data = app_zalloc_check(sizeof(SendTextAppData));
|
||||
app_state_set_user_data(data);
|
||||
|
||||
Window *window = &data->window;
|
||||
window_init(window, WINDOW_NAME("Send Text"));
|
||||
window_set_user_data(window, data);
|
||||
|
||||
Layer *window_root_layer = &window->layer;
|
||||
|
||||
prv_update_contact_list(data);
|
||||
|
||||
if (prv_has_contacts(data)) {
|
||||
const GRect menu_layer_frame =
|
||||
grect_inset(window_root_layer->bounds,
|
||||
GEdgeInsets(STATUS_BAR_LAYER_HEIGHT, 0,
|
||||
PBL_IF_ROUND_ELSE(STATUS_BAR_LAYER_HEIGHT, 0), 0));
|
||||
menu_layer_init(&data->menu_layer, &menu_layer_frame);
|
||||
menu_layer_set_callbacks(&data->menu_layer, data, &(MenuLayerCallbacks) {
|
||||
.get_num_rows = prv_contact_list_get_num_rows_callback,
|
||||
.get_cell_height = prv_contact_list_get_cell_height_callback,
|
||||
// On round we show the "Select Contact" text in a menu cell header, but on rect we show it
|
||||
// in the status bar (see below)
|
||||
#if PBL_ROUND
|
||||
.draw_header = prv_contact_list_draw_header_callback,
|
||||
.get_header_height = prv_contact_list_get_header_height_callback,
|
||||
#endif
|
||||
.draw_row = prv_contact_list_draw_row_callback,
|
||||
.select_click = prv_contact_list_select_callback,
|
||||
});
|
||||
|
||||
menu_layer_set_highlight_colors(&data->menu_layer, SEND_TEXT_APP_HIGHLIGHT_COLOR, 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));
|
||||
|
||||
StatusBarLayer *status_layer = &data->status_layer;
|
||||
status_bar_layer_init(status_layer);
|
||||
status_bar_layer_set_colors(status_layer, GColorClear, GColorBlack);
|
||||
// On rect we show the "Select Contact" text in the status bar, but on round the status bar
|
||||
// shows the clock time and we use a menu cell header to display "Select Contact" (see above)
|
||||
#if PBL_RECT
|
||||
status_bar_layer_set_title(status_layer, i18n_get("Select Contact", data), false /* revert */,
|
||||
false /* animate */);
|
||||
status_bar_layer_set_separator_mode(status_layer, StatusBarLayerSeparatorModeDotted);
|
||||
#endif
|
||||
layer_add_child(window_root_layer, status_bar_layer_get_layer(&data->status_layer));
|
||||
} else {
|
||||
PeekLayer *peek_layer = &data->no_contacts_layer;
|
||||
peek_layer_init(peek_layer, &data->window.layer.bounds);
|
||||
const GFont title_font = system_theme_get_font_for_default_size(TextStyleFont_Title);
|
||||
peek_layer_set_title_font(peek_layer, title_font);
|
||||
TimelineResourceInfo timeline_res = {
|
||||
.res_id = TIMELINE_RESOURCE_GENERIC_WARNING,
|
||||
};
|
||||
peek_layer_set_icon(peek_layer, &timeline_res);
|
||||
peek_layer_set_title(peek_layer, i18n_get("Add contacts in\nmobile app", data));
|
||||
peek_layer_set_background_color(peek_layer, GColorLightGray);
|
||||
peek_layer_play(peek_layer);
|
||||
layer_add_child(window_root_layer, &peek_layer->layer);
|
||||
}
|
||||
|
||||
const bool animated = true;
|
||||
app_window_stack_push(&data->window, animated);
|
||||
}
|
||||
|
||||
static void prv_deinit(void) {
|
||||
SendTextAppData *data = app_state_get_user_data();
|
||||
event_service_client_unsubscribe(&data->event_service_info);
|
||||
status_bar_layer_deinit(&data->status_layer);
|
||||
peek_layer_deinit(&data->no_contacts_layer);
|
||||
menu_layer_deinit(&data->menu_layer);
|
||||
i18n_free_all(data);
|
||||
prv_clear_contact_list(data);
|
||||
app_free(data);
|
||||
}
|
||||
|
||||
static void prv_main(void) {
|
||||
prv_init();
|
||||
app_event_loop();
|
||||
prv_deinit();
|
||||
}
|
||||
|
||||
const PebbleProcessMd *send_text_app_get_info(void) {
|
||||
static const PebbleProcessMdSystem s_send_text_app_info = {
|
||||
.common = {
|
||||
.main_func = prv_main,
|
||||
.uuid = UUID_SEND_TEXT_DATA_SOURCE,
|
||||
},
|
||||
.name = i18n_noop("Send Text"),
|
||||
.icon_resource_id = RESOURCE_ID_SEND_TEXT_APP_GLANCE,
|
||||
};
|
||||
|
||||
// If the phone doesn't support this app, we will act as if it's not installed by returning NULL
|
||||
const bool app_supported = send_text_service_is_send_text_supported();
|
||||
return app_supported ? (const PebbleProcessMd *)&s_send_text_app_info : NULL;
|
||||
}
|
21
src/fw/apps/system_apps/send_text/send_text.h
Normal file
21
src/fw/apps/system_apps/send_text/send_text.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 "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* send_text_app_get_info();
|
31
src/fw/apps/system_apps/send_text/send_text_app_prefs.h
Normal file
31
src/fw/apps/system_apps/send_text/send_text_app_prefs.h
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/attributes.h"
|
||||
#include "util/uuid.h"
|
||||
|
||||
typedef struct PACKED {
|
||||
Uuid contact_uuid;
|
||||
Uuid address_uuid;
|
||||
bool is_fav;
|
||||
} SerializedSendTextContact;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint8_t num_contacts;
|
||||
SerializedSendTextContact contacts[];
|
||||
} SerializedSendTextPrefs;
|
Loading…
Add table
Add a link
Reference in a new issue