pebble/src/fw/popups/phone_ui.c
Josh Soref 874cd587c2 spelling: existence
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2025-01-29 00:03:23 -05:00

1242 lines
44 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "phone_ui.h"
#include "phone_formatting.h"
#include "applib/fonts/fonts.h"
#include "util/trig.h"
#include "applib/ui/action_bar_layer.h"
#include "applib/ui/kino/kino_layer.h"
#include "applib/ui/kino/kino_reel_pdci.h"
#include "applib/ui/kino/kino_reel/morph_square.h"
#include "applib/ui/kino/kino_reel/transform.h"
#include "applib/ui/kino/kino_reel/unfold.h"
#include "applib/ui/ui.h"
#include "applib/ui/window_private.h"
#include "applib/ui/window_stack.h"
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "kernel/ui/kernel_ui.h"
#include "kernel/ui/modals/modal_manager.h"
#include "kernel/ui/system_icons.h"
#include "kernel/pbl_malloc.h"
#include "popups/notifications/notifications_presented_list.h"
#include "resource/resource_ids.auto.h"
#include "services/common/analytics/analytics.h"
#include "services/common/i18n/i18n.h"
#include "services/common/evented_timer.h"
#include "services/common/regular_timer.h"
#include "services/common/light.h"
#include "services/normal/blob_db/ios_notif_pref_db.h"
#include "services/normal/notifications/alerts.h"
#include "services/normal/notifications/notification_constants.h"
#include "services/normal/phone_call.h"
#include "services/normal/timeline/timeline.h"
#include "services/normal/timeline/timeline_actions.h"
#include "services/normal/timeline/timeline_resources.h"
#include "shell/system_theme.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/time/time.h"
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#if CAPABILITY_HAS_VIBE_SCORES
#include "services/normal/vibes/vibe_client.h"
#include "services/normal/vibes/vibe_score.h"
#endif
#define DECLINE_DELAY_MS 2000
#define SMS_REPLY_DELAY_MS 1200
#define SMS_REPLY_IOS_DELAY_MS 600
#define ACCEPT_DELAY_MS 3000
#define CALL_END_DELAY_MS 5000
#define OUTGOING_CALL_DELAY_MS 5000
#define MISSED_CALL_DELAY_MS 180000
#define NAME_BUFFER_LENGTH 32
#define CALL_STATUS_BUFFER_LENGTH 32
#define DEFAULT_COLOR PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite)
#define ACCEPT_COLOR PBL_IF_COLOR_ELSE(GColorIslamicGreen, GColorWhite)
#define DECLINE_COLOR PBL_IF_COLOR_ELSE(GColorRed, GColorWhite)
#define TEXT_MARGIN_WIDTH PBL_IF_RECT_ELSE(5, 10)
#define RIGHTSIDE_PADDING 18
#define TEXT_RIGHTSIDE_PADDING \
PBL_IF_RECT_ELSE(ACTION_BAR_WIDTH, ACTION_BAR_WIDTH + RIGHTSIDE_PADDING - TEXT_MARGIN_WIDTH)
#define ICON_WIDTH 80
#define ICON_POSITION_X \
PBL_IF_RECT_ELSE(18, DISP_ROWS - (ACTION_BAR_WIDTH + RIGHTSIDE_PADDING) - ICON_WIDTH)
#if PBL_ROUND
#define ICON_POSITION_CENTERED_X (DISP_ROWS / 2 - ICON_WIDTH / 2)
#endif
static const int16_t DOT_SIZE = 8;
static const uint32_t UNFOLD_DURATION = 300;
static const int16_t UNFOLD_EXPAND = 8;
static const uint32_t ANIMATION_FRAME_MS = 36;
static const uint32_t SQUARE_ANIMATION_FRAMES = 10;
static const uint32_t BOUNCEBACK_ANIMATION_FRAMES = 2;
static const uint32_t COLOUR_ANIMATION_FRAMES = 4;
static const uint32_t DURATION_APPEAR_ANIMATION_FRAMES = 4;
static const uint32_t ACTION_BAR_DISAPPEAR_ANIMATION_FRAMES = 2;
static const int16_t BOUNCEBACK_DISTANCE = 6;
static const int16_t DURATION_ANIMATION_START_OFFSET = 30;
static const int16_t SINGLE_LINE_BOUND_OFFSET = 5;
static const int16_t SINGLE_LINE_BOUND_HEIGHT = 30;
static const int16_t DOUBLE_LINE_BOUND_OFFSET = 0;
static const int16_t DOUBLE_LINE_BOUND_HEIGHT = 40;
//! Enumeration for the various action bar items in the phone ui
typedef enum {
PhoneCallActions_None = 0,
PhoneCallActions_Decline = 1 << 0,
PhoneCallActions_Answer = 1 << 1,
PhoneCallActions_Reply = 1 << 2,
} PhoneCallActions;
typedef struct {
TimelineResourceSize icon_size;
GPoint icon_pos;
int16_t caller_id_pos_y;
int16_t caller_id_height;
int16_t status_pos_y;
int16_t status_height;
bool large_caller_id;
} PhoneStyle;
typedef enum {
ACCEPTED,
DECLINED,
DISCONNECTED
} CallStatus;
typedef struct {
Window window;
#if !PLATFORM_TINTIN
struct {
GColor left;
GColor right;
int16_t boundary;
} bg_color;
Animation *action_bar_animation;
Animation *bg_color_animation;
Animation *call_status_animation;
#endif
ActionBarLayer action_bar;
Layer core_ui_container;
TextLayer caller_id_text_layer;
TextLayer call_status_text_layer;
StatusBarLayer status_bar;
KinoLayer icon_layer;
KinoReel *current_icon;
ResourceId current_icon_id;
bool hid_action_bar;
GBitmap up_bitmap;
GBitmap select_bitmap;
GBitmap down_bitmap;
ClickHandler up_action;
ClickHandler select_action;
ClickHandler down_action;
const PhoneStyle *style;
GFont name_font;
GFont long_name_font;
GFont status_font;
char caller_id_text_buf[NAME_BUFFER_LENGTH];
char call_status_text_buf[CALL_STATUS_BUFFER_LENGTH];
EventedTimerID call_duration_timer;
EventedTimerID window_pop_timer;
time_t call_start_time;
#if CAPABILITY_HAS_VIBE_SCORES
VibeScore *vibe_score;
#endif
RegularTimerInfo ring_timer;
bool show_ongoing_call_ui;
// Incoming call reply data
TimelineItem *call_response_item;
bool waiting_for_action_result;
bool open_reply_menu_on_pop;
void *action_handle;
} PhoneUIData;
static const PhoneStyle s_phone_style_default = {
.icon_size = TimelineResourceSizeLarge,
.icon_pos = { ICON_POSITION_X, PBL_IF_RECT_ELSE(25, 22) },
.caller_id_pos_y = PBL_IF_RECT_ELSE(102, 93),
.caller_id_height = 50,
.status_pos_y = PBL_IF_RECT_ELSE(142, 144),
.status_height = 20,
};
static const PhoneStyle s_phone_style_large = {
.icon_size = PBL_IF_RECT_ELSE(TimelineResourceSizeSmall, TimelineResourceSizeLarge),
.icon_pos = { ICON_POSITION_X, PBL_IF_RECT_ELSE(11, 22) },
.caller_id_pos_y = PBL_IF_RECT_ELSE(80, 88),
.caller_id_height = 60,
.status_pos_y = PBL_IF_RECT_ELSE(138, 144),
.status_height = 20,
.large_caller_id = true,
};
static const PhoneStyle *s_phone_styles[NumPreferredContentSizes] = {
[PreferredContentSizeSmall] = &s_phone_style_default,
[PreferredContentSizeMedium] = &s_phone_style_default,
[PreferredContentSizeLarge] = &s_phone_style_large,
[PreferredContentSizeExtraLarge] = &s_phone_style_large,
};
static PhoneUIData *s_phone_ui_data;
static void prv_window_pop(void);
static void prv_window_pop_with_delay(uint32_t delay_ms);
static void prv_action_bar_setup(PhoneCallActions actions);
static void prv_set_answer_window(void) {
modal_window_push(&s_phone_ui_data->window, ModalPriorityPhone, false /* don't animate */);
}
static void prv_set_reply_window(void) {
modal_window_push(&s_phone_ui_data->window, ModalPriorityNotification, false /* don't animate */);
}
//! Icon setters.
// This one will make sure the *previously* set icon resource is destroyed.
// The final icon set must still be destroyed alongside the reel.
static void prv_set_icon_resource(TimelineResourceId timeline_res_id) {
TimelineResourceInfo timeline_res = {
.res_id = timeline_res_id,
};
AppResourceInfo icon_res_info;
timeline_resources_get_id(&timeline_res, s_phone_ui_data->style->icon_size, &icon_res_info);
const ResourceId resource = icon_res_info.res_id;
// Resetting the same icon shouldn't be animated.
if (resource == s_phone_ui_data->current_icon_id) {
return;
}
KinoReel *new_image = kino_reel_create_with_resource(resource);
#if !PLATFORM_TINTIN
KinoReel *old_image = s_phone_ui_data->current_icon;
kino_layer_pause(&s_phone_ui_data->icon_layer);
KinoReel *icon_reel = kino_reel_morph_square_create(old_image, true);
kino_reel_transform_set_to_reel(icon_reel, new_image, false);
kino_reel_transform_set_transform_duration(icon_reel,
SQUARE_ANIMATION_FRAMES * ANIMATION_FRAME_MS);
kino_layer_set_reel(&s_phone_ui_data->icon_layer, icon_reel, true);
kino_layer_play(&s_phone_ui_data->icon_layer);
s_phone_ui_data->current_icon = new_image;
s_phone_ui_data->current_icon_id = resource;
#else
kino_layer_set_reel(&s_phone_ui_data->icon_layer, new_image, true);
s_phone_ui_data->current_icon = new_image;
s_phone_ui_data->current_icon_id = resource;
#endif
}
// This will do the wrong thing if called after the action bar is removed, due to the absolute
// coordinate scheme being offset.
static void prv_unfold_icon_resource(TimelineResourceId timeline_res_id) {
TimelineResourceInfo timeline_res = {
.res_id = timeline_res_id,
};
AppResourceInfo icon_res_info;
timeline_resources_get_id(&timeline_res, s_phone_ui_data->style->icon_size, &icon_res_info);
const ResourceId resource = icon_res_info.res_id;
KinoReel *image = kino_reel_create_with_resource(resource);
#if !PLATFORM_TINTIN
GRect layer_frame = s_phone_ui_data->icon_layer.layer.frame;
GSize size = kino_reel_get_size(image);
GRect icon_from = {
.origin.x = layer_frame.origin.x + (layer_frame.size.w - DOT_SIZE) / 2,
.origin.y = layer_frame.origin.y + (layer_frame.size.h - DOT_SIZE) / 2,
.size = { DOT_SIZE, DOT_SIZE },
};
GRect icon_to = {
.origin.x = layer_frame.origin.x + (layer_frame.size.w - size.w) / 2,
.origin.y = layer_frame.origin.y + (layer_frame.size.h - size.h) / 2,
.size = size,
};
KinoReel *kino_reel = kino_reel_unfold_create(image, false, layer_frame, 0,
UNFOLD_DEFAULT_NUM_DELAY_GROUPS,
UNFOLD_DEFAULT_GROUP_DELAY);
kino_reel_transform_set_from_frame(kino_reel, icon_from);
kino_reel_transform_set_to_frame(kino_reel, icon_to);
kino_reel_transform_set_transform_duration(kino_reel, UNFOLD_DURATION);
kino_reel_scale_segmented_set_deflate_effect(kino_reel, UNFOLD_EXPAND);
kino_layer_set_reel(&s_phone_ui_data->icon_layer, kino_reel, true);
kino_layer_play(&s_phone_ui_data->icon_layer);
s_phone_ui_data->current_icon = image;
s_phone_ui_data->current_icon_id = resource;
#else
kino_layer_set_reel(&s_phone_ui_data->icon_layer, image, true);
s_phone_ui_data->current_icon = image;
s_phone_ui_data->current_icon_id = resource;
#endif
}
#if !PLATFORM_TINTIN
static void prv_update_color_boundary(void *subject, int16_t boundary) {
s_phone_ui_data->bg_color.boundary = boundary;
layer_mark_dirty(&s_phone_ui_data->window.layer);
}
static int16_t prv_get_color_boundary(void *subject) {
return s_phone_ui_data->bg_color.boundary;
}
static const PropertyAnimationImplementation s_color_slide_animation_impl = {
.base = {
.update = (AnimationUpdateImplementation)property_animation_update_int16,
},
.accessors = {
.getter = { .int16 = (const Int16Getter)prv_get_color_boundary },
.setter = { .int16 = (const Int16Setter)prv_update_color_boundary, },
},
};
#endif
static void prv_set_window_color(GColor color, bool left_to_right) {
#if !PLATFORM_TINTIN
Animation *color_animation;
int16_t width = s_phone_ui_data->window.layer.bounds.size.w;
int16_t zero = 0;
animation_unschedule(s_phone_ui_data->bg_color_animation);
// Take whichever side is more complete as our starting colour.
if (s_phone_ui_data->bg_color.boundary > width / 2) {
s_phone_ui_data->bg_color.right = s_phone_ui_data->bg_color.left;
} else {
s_phone_ui_data->bg_color.left = s_phone_ui_data->bg_color.right;
}
if (left_to_right) {
s_phone_ui_data->bg_color.left = color;
color_animation = property_animation_get_animation(
property_animation_create(&s_color_slide_animation_impl, NULL, &zero, &width));
s_phone_ui_data->bg_color.boundary = 0;
} else {
s_phone_ui_data->bg_color.right = color;
color_animation = property_animation_get_animation(
property_animation_create(&s_color_slide_animation_impl, NULL, &width, &zero));
s_phone_ui_data->bg_color.boundary = width;
}
s_phone_ui_data->bg_color_animation = color_animation;
animation_set_duration(color_animation, COLOUR_ANIMATION_FRAMES * ANIMATION_FRAME_MS);
animation_set_curve(color_animation, AnimationCurveEaseIn);
animation_schedule(color_animation);
#else
layer_mark_dirty(&s_phone_ui_data->window.layer);
#endif
}
// Names can sometimes actually be phone numbers. We're assuming that phone numbers will always
// match /^[() +0-9-]+$/
static bool prv_is_string_a_phone_number(const char *name) {
size_t length = strlen(name);
// Blocked/unknown numbers on Android start with a '-'
if ((name[0] == '-') || (length == 0)) {
return false;
}
for (size_t i = 0; i < length; ++i) {
unsigned char chr = name[i];
if ((chr != '(') && (chr != ')') && (chr != '+') && (chr != ' ') && (chr != '-') && (chr != '.')
&& !isdigit(chr)) {
return false;
}
}
return true;
}
static bool prv_has_long_name(GFont font) {
// Figure out if it's a "long name"
// (i.e. one that won't fit a single line at the default font size).
const int16_t fudge_some_pixels = 30;
const bool line_contains_newline = (strchr(s_phone_ui_data->caller_id_text_buf, '\n') != NULL);
int16_t test_width = s_phone_ui_data->caller_id_text_layer.layer.bounds.size.w
+ fudge_some_pixels;
GSize text_size = graphics_text_layout_get_max_used_size(
kernel_ui_get_graphics_context(),
s_phone_ui_data->caller_id_text_buf,
font,
GRect(0, 0, test_width, SINGLE_LINE_BOUND_HEIGHT),
s_phone_ui_data->caller_id_text_layer.overflow_mode,
GTextAlignmentLeft, NULL);
return (text_size.w > s_phone_ui_data->caller_id_text_layer.layer.bounds.size.w) ||
line_contains_newline;
}
//! Text setters
static void prv_set_caller_id_text(PebblePhoneCaller *caller) {
if (!caller->name && !caller->number) {
return;
}
const char *caller_text = caller->name ?: caller->number;
// Occasionally a name comes in as a number, and vice versa
const bool is_phone_number = prv_is_string_a_phone_number(caller_text);
GFont caller_id_font = NULL;
int lines = 1;
if (is_phone_number) {
phone_format_phone_number(caller_text, s_phone_ui_data->caller_id_text_buf, NAME_BUFFER_LENGTH);
text_layer_set_overflow_mode(&s_phone_ui_data->caller_id_text_layer, GTextOverflowModeWordWrap);
} else {
phone_format_caller_name(caller_text, s_phone_ui_data->caller_id_text_buf, NAME_BUFFER_LENGTH);
}
if (s_phone_ui_data->style->large_caller_id) {
caller_id_font = s_phone_ui_data->name_font;
lines++;
} else if (prv_has_long_name(s_phone_ui_data->name_font)) {
caller_id_font = s_phone_ui_data->long_name_font;
lines++;
} else {
caller_id_font = s_phone_ui_data->name_font;
}
text_layer_set_font(&s_phone_ui_data->caller_id_text_layer, caller_id_font);
if (lines == 1) {
s_phone_ui_data->caller_id_text_layer.layer.bounds.origin.y = SINGLE_LINE_BOUND_OFFSET;
} else {
s_phone_ui_data->caller_id_text_layer.layer.bounds.origin.y = DOUBLE_LINE_BOUND_OFFSET;
}
s_phone_ui_data->caller_id_text_layer.layer.bounds.size.h =
lines * fonts_get_font_height(caller_id_font);
text_layer_set_text(&s_phone_ui_data->caller_id_text_layer, s_phone_ui_data->caller_id_text_buf);
}
//! Window background rendering
static void prv_window_update_proc(Layer *layer, GContext *ctx) {
#if !PLATFORM_TINTIN
graphics_context_set_fill_color(ctx, s_phone_ui_data->bg_color.left);
graphics_fill_rect(ctx, &GRect(0, 0, s_phone_ui_data->bg_color.boundary, layer->bounds.size.h));
graphics_context_set_fill_color(ctx, s_phone_ui_data->bg_color.right);
graphics_fill_rect(ctx, &GRect(s_phone_ui_data->bg_color.boundary, 0, layer->bounds.size.w,
layer->bounds.size.h));
#else
graphics_context_set_fill_color(ctx, DEFAULT_COLOR);
graphics_fill_rect(ctx, &layer->bounds);
#endif
}
//! Ring functionality
static void prv_ring(void *unused) {
PBL_LOG(LOG_LEVEL_DEBUG, "RING");
if (alerts_should_vibrate_for_type(AlertPhoneCall)) {
#if CAPABILITY_HAS_VIBE_SCORES
if (!s_phone_ui_data || !s_phone_ui_data->vibe_score) {
// There is a mutex-related issue that can appear where the timer callback will execute after
// phone_ui cancels the timer and frees the vibe_score / s_phone_ui_data. Thus, bail early
// if we detect this bad state.
// See PBL-35548
return;
}
vibe_score_do_vibe(s_phone_ui_data->vibe_score);
#else
vibes_long_pulse();
#endif
}
if (alerts_should_enable_backlight_for_type(AlertPhoneCall)) {
light_enable_interaction();
}
}
static void prv_start_ringing(void) {
alerts_incoming_alert_analytics();
s_phone_ui_data->ring_timer = (const RegularTimerInfo) {
.cb = prv_ring,
};
unsigned int vibe_repeat_interval_sec;
#if CAPABILITY_HAS_VIBE_SCORES
s_phone_ui_data->vibe_score = vibe_client_get_score(VibeClient_PhoneCalls);
if (!s_phone_ui_data->vibe_score) {
return;
}
unsigned int vibe_interval_ms = vibe_score_get_duration_ms(s_phone_ui_data->vibe_score) +
vibe_score_get_repeat_delay_ms(s_phone_ui_data->vibe_score);
vibe_repeat_interval_sec = DIVIDE_CEIL(vibe_interval_ms, MS_PER_SECOND);
#else
vibe_repeat_interval_sec = 2;
#endif
prv_ring(NULL);
regular_timer_add_multisecond_callback(&s_phone_ui_data->ring_timer, vibe_repeat_interval_sec);
}
static void prv_stop_ringing(void) {
regular_timer_remove_callback(&s_phone_ui_data->ring_timer);
#if CAPABILITY_HAS_VIBE_SCORES
if (s_phone_ui_data->vibe_score) {
vibe_score_destroy(s_phone_ui_data->vibe_score);
s_phone_ui_data->vibe_score = NULL;
}
#endif
vibes_cancel();
}
//! Call duration related functions
static void prv_show_call_status(void) {
layer_set_hidden(&s_phone_ui_data->call_status_text_layer.layer, false);
#if !PLATFORM_TINTIN
s_phone_ui_data->call_status_text_layer.layer.bounds.origin.y = DURATION_ANIMATION_START_OFFSET;
Animation *upward = property_animation_get_animation(
property_animation_create_bounds_origin(&s_phone_ui_data->call_status_text_layer.layer,
&GPoint(0, DURATION_ANIMATION_START_OFFSET),
&GPoint(0, -BOUNCEBACK_DISTANCE)));
animation_set_curve(upward, AnimationCurveEaseIn);
animation_set_duration(upward, DURATION_APPEAR_ANIMATION_FRAMES * ANIMATION_FRAME_MS);
Animation *bounceback = property_animation_get_animation(
property_animation_create_bounds_origin(&s_phone_ui_data->call_status_text_layer.layer,
&GPoint(0, -BOUNCEBACK_DISTANCE),
&GPointZero));
animation_set_curve(bounceback, AnimationCurveEaseOut);
animation_set_duration(bounceback, BOUNCEBACK_ANIMATION_FRAMES * ANIMATION_FRAME_MS);
Animation *animation = animation_sequence_create(upward, bounceback, NULL);
s_phone_ui_data->call_status_animation = animation;
animation_schedule(animation);
#else
s_phone_ui_data->call_status_text_layer.layer.bounds.origin = GPointZero;
#endif
}
static void prv_update_call_time(void *unused) {
if (s_phone_ui_data == NULL) {
return;
}
if (layer_get_hidden(&s_phone_ui_data->call_status_text_layer.layer)) {
prv_show_call_status();
}
const time_t duration = rtc_get_time() - s_phone_ui_data->call_start_time;
const int seconds = duration % SECONDS_PER_MINUTE;
int minutes = (duration - seconds) / SECONDS_PER_MINUTE;
if (minutes >= MINUTES_PER_HOUR) {
const int hours = minutes / MINUTES_PER_HOUR;
minutes = minutes % MINUTES_PER_HOUR;
sniprintf(s_phone_ui_data->call_status_text_buf, CALL_STATUS_BUFFER_LENGTH,
"%u:%02u:%02u", hours, minutes, seconds);
} else {
sniprintf(s_phone_ui_data->call_status_text_buf, CALL_STATUS_BUFFER_LENGTH,
"%u:%02u", minutes, seconds);
}
text_layer_set_text(&s_phone_ui_data->call_status_text_layer,
s_phone_ui_data->call_status_text_buf);
}
static void prv_start_call_duration_timer(void) {
if (s_phone_ui_data->call_start_time == 0) {
s_phone_ui_data->call_start_time = rtc_get_time();
}
s_phone_ui_data->call_duration_timer = evented_timer_register(
1000, true /* repeating */, prv_update_call_time, NULL);
// Update call time immediately
prv_update_call_time(NULL);
}
static void prv_stop_call_duration_timer(void) {
evented_timer_cancel(s_phone_ui_data->call_duration_timer);
s_phone_ui_data->call_duration_timer = EVENTED_TIMER_INVALID_ID;
}
static void prv_set_status_text(CallStatus status) {
if (status == ACCEPTED) {
i18n_get_with_buffer("Call Accepted", s_phone_ui_data->call_status_text_buf,
CALL_STATUS_BUFFER_LENGTH);
} else if (status == DISCONNECTED) {
i18n_get_with_buffer("Disconnected", s_phone_ui_data->call_status_text_buf,
CALL_STATUS_BUFFER_LENGTH);
} else {
if (s_phone_ui_data->call_start_time) {
i18n_get_with_buffer("Call Ended", s_phone_ui_data->call_status_text_buf,
CALL_STATUS_BUFFER_LENGTH);
} else {
i18n_get_with_buffer("Call Declined", s_phone_ui_data->call_status_text_buf,
CALL_STATUS_BUFFER_LENGTH);
}
}
text_layer_set_text(&s_phone_ui_data->call_status_text_layer,
s_phone_ui_data->call_status_text_buf);
prv_show_call_status();
}
// Handles cleanup when the SMS reply menu closes
static void prv_action_menu_did_close(ActionMenu *action_menu, const ActionMenuItem *item,
void *context) {
timeline_item_destroy(context);
}
static void prv_ancs_response_action_result_handler(bool success, void *timeline_item) {
timeline_item_destroy(timeline_item);
// We got the action result for our iOS reply. We can now close the phone ui window because we
// are displaying the action menu (but only if the original window hasn't already been torn down)
if (s_phone_ui_data && s_phone_ui_data->waiting_for_action_result) {
prv_window_pop();
}
}
// Creates a new reply action menu and pushes it with notification modal priority
static void prv_open_reply_action_menu(void *unused) {
// Drop the call window priority so we properly animate in the menu
prv_set_reply_window();
// The timeline item will be cleaned up by the action menu/action callbacks
TimelineItem *item = s_phone_ui_data->call_response_item;
s_phone_ui_data->call_response_item = NULL;
TimelineItemAction *reply_action = timeline_item_find_reply_action(item);
if (!reply_action) {
return;
}
switch (reply_action->type) {
case TimelineItemActionTypeResponse:
timeline_actions_push_response_menu(item, reply_action, SMS_REPLY_COLOR,
prv_action_menu_did_close,
modal_manager_get_window_stack(ModalPriorityNotification),
TimelineItemActionSourcePhoneUi,
true /* standalone_reply */);
break;
case TimelineItemActionTypeAncsResponse:
// Mark this window so we know to pop it when we get a response
s_phone_ui_data->waiting_for_action_result = true;
// Kick off the reply action automatically - we will pop the phone ui once we get an action
// result and can show the action menu
timeline_actions_invoke_action(reply_action, item, prv_ancs_response_action_result_handler,
item);
break;
default:
break;
}
}
//! Action bar click handlers
static void prv_answer_click_handler(ClickRecognizerRef recognizer, void *unused) {
prv_stop_ringing();
phone_call_answer();
// This must be called before prv_set_status_text, otherwise the text will not be centered
prv_action_bar_setup(PhoneCallActions_None);
prv_set_window_color(ACCEPT_COLOR, false);
prv_set_icon_resource(TIMELINE_RESOURCE_DURING_PHONE_CALL);
if (s_phone_ui_data->show_ongoing_call_ui) {
prv_start_call_duration_timer();
} else {
prv_set_status_text(ACCEPTED);
prv_window_pop_with_delay(ACCEPT_DELAY_MS);
}
prv_set_answer_window();
}
static void prv_decline_call(void) {
prv_stop_ringing();
phone_call_decline();
prv_stop_call_duration_timer();
prv_set_icon_resource(TIMELINE_RESOURCE_DISMISSED_PHONE_CALL);
prv_set_window_color(DECLINE_COLOR, true);
// This must be called before prv_set_status_text, otherwise the text will not be centered
prv_action_bar_setup(PhoneCallActions_None);
prv_set_status_text(DECLINED);
}
static void prv_decline_click_handler(ClickRecognizerRef recognizer, void *unused) {
prv_decline_call();
prv_window_pop_with_delay(DECLINE_DELAY_MS);
}
static void prv_sms_reply_click_handler(ClickRecognizerRef recognizer, void *unused) {
prv_decline_call();
s_phone_ui_data->open_reply_menu_on_pop = true;
const TimelineItemAction *reply_action =
timeline_item_find_reply_action(s_phone_ui_data->call_response_item);
switch (reply_action->type) {
case TimelineItemActionTypeResponse:
// On Android, we just open the action menu at the same time we pop the window
prv_window_pop_with_delay(SMS_REPLY_DELAY_MS);
break;
case TimelineItemActionTypeAncsResponse:
// On iOS, show the "Call Declined" animation and send the AncsResponse message shortly after
// We hold the phone UI up until timeline_actions responds or another call comes in
s_phone_ui_data->window_pop_timer = evented_timer_register(SMS_REPLY_IOS_DELAY_MS,
false /* repeating */, prv_open_reply_action_menu, NULL);
break;
default:
break;
}
}
static void prv_pop_click_handler(ClickRecognizerRef recognizer, void *unused) {
analytics_inc(ANALYTICS_DEVICE_METRIC_PHONE_CALL_POP_COUNT, AnalyticsClient_System);
prv_stop_ringing();
prv_window_pop();
}
//! Action bar animation
static void prv_hide_action_bar(void) {
if (s_phone_ui_data->hid_action_bar) {
return;
}
s_phone_ui_data->hid_action_bar = true;
#if !PLATFORM_TINTIN
const GRect window_bounds = s_phone_ui_data->window.layer.bounds;
GRect offscreen = GRect(window_bounds.size.w, 0, PBL_IF_RECT_ELSE(ACTION_BAR_WIDTH, 0),
window_bounds.size.h);
Animation *action_bar_animation = property_animation_get_animation(
property_animation_create_layer_frame(&s_phone_ui_data->action_bar.layer, NULL, &offscreen));
animation_set_duration(action_bar_animation, ACTION_BAR_DISAPPEAR_ANIMATION_FRAMES
* ANIMATION_FRAME_MS);
animation_set_curve(action_bar_animation, AnimationCurveEaseIn);
GPoint overshoot = GPoint(PBL_IF_RECT_ELSE(ACTION_BAR_WIDTH / 2, 0) + BOUNCEBACK_DISTANCE, 0);
Animation *ui_movement = property_animation_get_animation(
property_animation_create_bounds_origin(&s_phone_ui_data->core_ui_container, NULL,
&overshoot));
animation_set_curve(ui_movement, AnimationCurveEaseIn);
animation_set_duration(ui_movement, 3 * ANIMATION_FRAME_MS);
Animation *ui_bounceback = property_animation_get_animation(
property_animation_create_bounds_origin(
&s_phone_ui_data->core_ui_container, &overshoot,
&GPoint(PBL_IF_RECT_ELSE(ACTION_BAR_WIDTH / 2, 0), 0)));
animation_set_curve(ui_bounceback, AnimationCurveEaseOut);
animation_set_duration(ui_bounceback, 2 * ANIMATION_FRAME_MS);
Animation *ui_animation = animation_sequence_create(ui_movement, ui_bounceback, NULL);
Animation *combined = animation_spawn_create(action_bar_animation, ui_animation, NULL);
s_phone_ui_data->action_bar_animation = combined;
animation_schedule(combined);
#if PBL_ROUND
// Extend the bounds to center the call text when the action bar is removed
s_phone_ui_data->caller_id_text_layer.layer.bounds.size.w += TEXT_RIGHTSIDE_PADDING;
text_layer_set_text_alignment(&s_phone_ui_data->caller_id_text_layer, GTextAlignmentCenter);
s_phone_ui_data->call_status_text_layer.layer.bounds.size.w += TEXT_RIGHTSIDE_PADDING;
text_layer_set_text_alignment(&s_phone_ui_data->call_status_text_layer, GTextAlignmentCenter);
// Center the kino icon
s_phone_ui_data->icon_layer.layer.frame.origin.x = ICON_POSITION_CENTERED_X;
#endif
#else
const GRect container_bounds = s_phone_ui_data->core_ui_container.bounds;
const GRect onscreen = GRect(ACTION_BAR_WIDTH / 2, 0,
container_bounds.size.w, container_bounds.size.h);
layer_set_hidden(&s_phone_ui_data->action_bar.layer, true /* hide */);
layer_set_bounds(&s_phone_ui_data->core_ui_container, &onscreen);
#endif
}
//! Action bar setup functions
static void prv_set_action_bar_icon(ButtonId button, ResourceId resource, GBitmap *storage) {
if (resource == RESOURCE_ID_INVALID) {
action_bar_layer_clear_icon(&s_phone_ui_data->action_bar, button);
return;
}
gbitmap_deinit(storage);
gbitmap_init_with_resource_system(storage, SYSTEM_APP, resource);
action_bar_layer_set_icon(&s_phone_ui_data->action_bar, button, storage);
}
// Returns the appropriate app id for the given phone number and call source
static const char *prv_get_app_id(const char *number, PhoneCallSource source) {
if (!number) {
return NULL;
}
// Select appropriate app id
switch (source) {
case PhoneCallSource_PP:
// We require the this to be a valid number when coming from PP
if (prv_is_string_a_phone_number(number)) {
return ANDROID_PHONE_KEY;
}
break;
case PhoneCallSource_ANCS:
case PhoneCallSource_ANCS_Legacy:
return IOS_PHONE_KEY;
break;
}
return NULL;
}
// Checks for the existence of a call reply action in the notif pref db and loads it into
// a timeline item
static bool prv_load_sms_reply_action(const char *number, PhoneCallSource source) {
const char *app_id = prv_get_app_id(number, source);
if (!app_id) {
return false;
}
// Load actions from prefs db and determine if we have an SMS reply option
iOSNotifPrefs *notif_prefs = ios_notif_pref_db_get_prefs((uint8_t *)app_id, strlen(app_id));
if (!notif_prefs) {
return false;
}
// Add attributes to the timeline item for contact lookup
AttributeList attributes = {};
attribute_list_add_cstring(&attributes, AttributeIdSender, number);
attribute_list_add_cstring(&attributes, AttributeIdiOSAppIdentifier, app_id);
TimelineItem *item = timeline_item_create_with_attributes(0, 0, TimelineItemTypeNotification,
LayoutIdUnknown,
&attributes,
&notif_prefs->action_group);
bool rv = false;
// Make sure we have a reply action (this properly handles NULL items)
const TimelineItemAction *reply_action = timeline_item_find_reply_action(item);
if (reply_action) {
s_phone_ui_data->call_response_item = item;
rv = true;
if (reply_action->type == TimelineItemActionTypeResponse) {
item->header.id = (Uuid)UUID_SEND_SMS;
}
} else {
timeline_item_destroy(item);
}
attribute_list_destroy_list(&attributes);
ios_notif_pref_db_free_prefs(notif_prefs);
return rv;
}
//! Action bar click configurations
static void prv_click_config_provider(void *context) {
if (s_phone_ui_data->up_action) {
window_single_click_subscribe(BUTTON_ID_UP, s_phone_ui_data->up_action);
}
if (s_phone_ui_data->select_action) {
window_single_click_subscribe(BUTTON_ID_SELECT, s_phone_ui_data->select_action);
}
if (s_phone_ui_data->down_action) {
window_single_click_subscribe(BUTTON_ID_DOWN, s_phone_ui_data->down_action);
}
window_single_click_subscribe(BUTTON_ID_BACK, prv_pop_click_handler);
}
static void prv_action_bar_setup(PhoneCallActions actions) {
s_phone_ui_data->up_action = NULL;
s_phone_ui_data->select_action = NULL;
s_phone_ui_data->down_action = NULL;
ResourceId up_icon = RESOURCE_ID_INVALID;
ResourceId select_icon = RESOURCE_ID_INVALID;
ResourceId down_icon = RESOURCE_ID_INVALID;
if (actions) {
if (actions & PhoneCallActions_Answer) {
s_phone_ui_data->up_action = prv_answer_click_handler;
up_icon = RESOURCE_ID_ACTION_BAR_ICON_CHECK;
}
if (actions & PhoneCallActions_Reply) {
// Move to top if that place isn't taken
if (!s_phone_ui_data->up_action) {
s_phone_ui_data->up_action = prv_sms_reply_click_handler;
up_icon = RESOURCE_ID_ACTION_BAR_ICON_SMS;
} else {
s_phone_ui_data->select_action = prv_sms_reply_click_handler;
select_icon = RESOURCE_ID_ACTION_BAR_ICON_SMS;
}
}
if (actions & PhoneCallActions_Decline) {
s_phone_ui_data->down_action = prv_decline_click_handler;
down_icon = RESOURCE_ID_ACTION_BAR_ICON_X;
}
prv_set_action_bar_icon(BUTTON_ID_UP, up_icon, &s_phone_ui_data->up_bitmap);
prv_set_action_bar_icon(BUTTON_ID_SELECT, select_icon, &s_phone_ui_data->select_bitmap);
prv_set_action_bar_icon(BUTTON_ID_DOWN, down_icon, &s_phone_ui_data->down_bitmap);
} else {
prv_hide_action_bar();
}
action_bar_layer_set_click_config_provider(&s_phone_ui_data->action_bar,
prv_click_config_provider);
}
//! Put the correct data in the 3 text fields
static void prv_display_caller_info(PebblePhoneCaller *caller) {
prv_set_caller_id_text(caller);
}
static void prv_phone_ui_deinit(void) {
if (s_phone_ui_data == NULL) {
return;
}
kino_layer_pause(&s_phone_ui_data->icon_layer);
kino_layer_deinit(&s_phone_ui_data->icon_layer);
#if !PLATFORM_TINTIN
// The reels will destroy intermediate images, but not the one currently on screen
// clean it up here. Note that we don't have to do this on Tintin/Bianca as we
// do not create an intermediary reel for animating.
kino_reel_destroy(s_phone_ui_data->current_icon);
animation_unschedule(s_phone_ui_data->bg_color_animation);
animation_unschedule(s_phone_ui_data->action_bar_animation);
animation_unschedule(s_phone_ui_data->call_status_animation);
#endif
s_phone_ui_data->current_icon = NULL;
s_phone_ui_data->current_icon_id = 0;
status_bar_layer_deinit(&s_phone_ui_data->status_bar);
gbitmap_deinit(&s_phone_ui_data->up_bitmap);
gbitmap_deinit(&s_phone_ui_data->select_bitmap);
gbitmap_deinit(&s_phone_ui_data->down_bitmap);
text_layer_deinit(&s_phone_ui_data->call_status_text_layer);
text_layer_deinit(&s_phone_ui_data->caller_id_text_layer);
evented_timer_cancel(s_phone_ui_data->call_duration_timer);
evented_timer_cancel(s_phone_ui_data->window_pop_timer);
action_bar_layer_deinit(&s_phone_ui_data->action_bar);
i18n_free_all(s_phone_ui_data);
prv_stop_ringing();
window_deinit(&s_phone_ui_data->window);
timeline_item_destroy(s_phone_ui_data->call_response_item);
kernel_free(s_phone_ui_data);
s_phone_ui_data = NULL;
}
static void prv_handle_window_unload(Window *window) {
prv_phone_ui_deinit();
}
//! Window destroy functions
//! Currently only 1 call window can exist at a time
static void prv_window_pop(void) {
if (s_phone_ui_data == NULL) {
// Check to make sure we didn't get popped already.
// There could possibly be 2 of these callback in the queue at time if this is called right after
// a prv_pop_with_delay
return;
}
if (s_phone_ui_data->open_reply_menu_on_pop) {
prv_open_reply_action_menu(NULL);
}
window_stack_remove(&s_phone_ui_data->window, true /* animated */);
// The window_stack_remove() call should run the unload handler (which deinits the ui),
// but in the rare case that the window never loaded (i.e. a higher priority modal was up)
// then we could leak the phone_ui data and assert on the next phone call.
// Deinit again to cover this case (will be a no-op) if the window was already deinited.
prv_phone_ui_deinit();
}
static void prv_window_pop_cb(void *unused) {
s_phone_ui_data->window_pop_timer = EVENTED_TIMER_INVALID_ID;
prv_window_pop();
}
static void prv_window_pop_with_delay(uint32_t delay_ms) {
s_phone_ui_data->window_pop_timer = evented_timer_register(
delay_ms, false /* repeating */, prv_window_pop_cb, NULL);
}
//! Window setup
//! Currently only 1 call window can exist at a time
static void prv_phone_ui_init(void) {
PBL_ASSERTN(s_phone_ui_data == NULL);
s_phone_ui_data = kernel_zalloc_check(sizeof(PhoneUIData));
s_phone_ui_data->hid_action_bar = false;
s_phone_ui_data->style = s_phone_styles[system_theme_get_content_size()];
const PhoneStyle *style = s_phone_ui_data->style;
s_phone_ui_data->name_font = system_theme_get_font(TextStyleFont_Title);
s_phone_ui_data->long_name_font =
system_theme_get_font(PBL_IF_RECT_ELSE(TextStyleFont_Header, TextStyleFont_Title));
s_phone_ui_data->status_font =
PBL_IF_RECT_ELSE(system_theme_get_font(TextStyleFont_Header),
fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
Window *window = &s_phone_ui_data->window;
window_init(window, WINDOW_NAME("Phone"));
window_set_status_bar_icon(window, (GBitmap*)&s_status_icon_phone_bitmap);
layer_set_update_proc(&window->layer, prv_window_update_proc);
window_set_window_handlers(&s_phone_ui_data->window, &(WindowHandlers) {
.unload = prv_handle_window_unload,
});
window_set_overrides_back_button(window, true);
#if !PLATFORM_TINTIN
s_phone_ui_data->bg_color.left = DEFAULT_COLOR;
s_phone_ui_data->bg_color.right = DEFAULT_COLOR;
s_phone_ui_data->bg_color.boundary = 0;
#endif
const int16_t width = window->layer.bounds.size.w - (TEXT_MARGIN_WIDTH * 2);
// Container layer
layer_init(&s_phone_ui_data->core_ui_container, &window->layer.bounds);
layer_add_child(&window->layer, &s_phone_ui_data->core_ui_container);
// Status bar
status_bar_layer_init(&s_phone_ui_data->status_bar);
layer_set_frame(&s_phone_ui_data->status_bar.layer,
&GRect(0, 0, window->layer.bounds.size.w - PBL_IF_RECT_ELSE(ACTION_BAR_WIDTH, 0),
STATUS_BAR_LAYER_HEIGHT));
status_bar_layer_set_colors(&s_phone_ui_data->status_bar,
PBL_IF_COLOR_ELSE(GColorClear, GColorWhite),
GColorBlack);
layer_add_child(&s_phone_ui_data->core_ui_container, &s_phone_ui_data->status_bar.layer);
// Icon
kino_layer_init(&s_phone_ui_data->icon_layer,
&(GRect){ style->icon_pos, { ICON_WIDTH, ICON_WIDTH } });
kino_layer_set_alignment(&s_phone_ui_data->icon_layer, GAlignCenter);
layer_add_child(&s_phone_ui_data->core_ui_container, &s_phone_ui_data->icon_layer.layer);
// Caller ID text
const GRect caller_id_text_rect = GRect(TEXT_MARGIN_WIDTH, style->caller_id_pos_y,
width, style->caller_id_height);
text_layer_init_with_parameters(&s_phone_ui_data->caller_id_text_layer,
&caller_id_text_rect, NULL, NULL,
GColorBlack,
PBL_IF_COLOR_ELSE(GColorClear, GColorWhite),
PBL_IF_RECT_ELSE(GTextAlignmentCenter, GTextAlignmentRight),
GTextOverflowModeTrailingEllipsis);
layer_add_child(&s_phone_ui_data->core_ui_container,
&s_phone_ui_data->caller_id_text_layer.layer);
// Shrink the bounds but not the frame size to allow for centering when action bar removed
s_phone_ui_data->caller_id_text_layer.layer.bounds.size.w = width - TEXT_RIGHTSIDE_PADDING;
// Status text
const GRect call_status_text_rect = GRect(TEXT_MARGIN_WIDTH, style->status_pos_y,
width, style->status_height);
text_layer_init_with_parameters(&s_phone_ui_data->call_status_text_layer,
&call_status_text_rect, NULL, s_phone_ui_data->status_font,
GColorBlack,
PBL_IF_COLOR_ELSE(GColorClear, GColorWhite),
PBL_IF_RECT_ELSE(GTextAlignmentCenter, GTextAlignmentRight),
GTextOverflowModeTrailingEllipsis);
layer_set_hidden(&s_phone_ui_data->call_status_text_layer.layer, true);
layer_set_clips(&s_phone_ui_data->call_status_text_layer.layer, false);
layer_add_child(&s_phone_ui_data->core_ui_container,
&s_phone_ui_data->call_status_text_layer.layer);
// Shrink the bounds but not the frame size to allow for centering when action bar removed
s_phone_ui_data->call_status_text_layer.layer.bounds.size.w = width - TEXT_RIGHTSIDE_PADDING;
// Action bar
action_bar_layer_init(&s_phone_ui_data->action_bar);
action_bar_layer_add_to_window(&s_phone_ui_data->action_bar, window);
modal_window_push(window, ModalPriorityCritical, true /* animated */);
}
static bool prv_check_popups_are_blocked(void) {
if (launcher_popups_are_blocked()) {
PBL_LOG(LOG_LEVEL_INFO, "Ignoring call event. Popups are blocked");
return true;
}
return false;
}
//!
//! API for updating / creating the phone UI
//!
void phone_ui_handle_incoming_call(PebblePhoneCaller *caller, bool can_answer,
bool show_ongoing_call_ui, PhoneCallSource source) {
if (prv_check_popups_are_blocked()) {
return;
}
if (s_phone_ui_data) {
// In this case we are waiting to pop the window and a new event has come in.
// Pop it immediately and then set up for the new event
prv_window_pop();
}
prv_phone_ui_init();
s_phone_ui_data->show_ongoing_call_ui = show_ongoing_call_ui;
prv_unfold_icon_resource(TIMELINE_RESOURCE_INCOMING_PHONE_CALL);
bool can_reply = false;
if (caller) {
prv_display_caller_info(caller);
// Check if we support sms reply
can_reply = prv_load_sms_reply_action(caller->number, source);
}
uint8_t actions = PhoneCallActions_Decline;
if (can_reply) {
actions |= PhoneCallActions_Reply;
}
if (can_answer) {
actions |= PhoneCallActions_Answer;
}
prv_action_bar_setup(actions);
prv_start_ringing();
}
void phone_ui_handle_outgoing_call(PebblePhoneCaller *caller) {
if (s_phone_ui_data) {
// In this case we are waiting to pop the window and a new event has come in.
// Pop it immediately and then set up for the new event
prv_window_pop();
}
prv_phone_ui_init();
// FIXME: PBL-21570 Outgoing call small is missing
prv_unfold_icon_resource(TIMELINE_RESOURCE_INCOMING_PHONE_CALL);
if (caller) {
prv_display_caller_info(caller);
}
prv_action_bar_setup(PhoneCallActions_None);
prv_window_pop_with_delay(OUTGOING_CALL_DELAY_MS);
}
void phone_ui_handle_missed_call(void) {
if (!s_phone_ui_data) {
return;
}
prv_stop_ringing();
prv_set_icon_resource(TIMELINE_RESOURCE_DISMISSED_PHONE_CALL);
prv_action_bar_setup(PhoneCallActions_None);
prv_window_pop_with_delay(MISSED_CALL_DELAY_MS);
}
void phone_ui_handle_call_start(bool can_decline) {
if (!s_phone_ui_data) {
PBL_LOG(LOG_LEVEL_ERROR, "Can't handle call start, UI isn't setup");
return;
}
prv_stop_ringing();
#if PBL_RECT
prv_set_icon_resource(TIMELINE_RESOURCE_DURING_PHONE_CALL);
#else
// action bar requires right-aligned icon, otherwise centered icon
prv_set_icon_resource((can_decline) ? TIMELINE_RESOURCE_DURING_PHONE_CALL :
TIMELINE_RESOURCE_DURING_PHONE_CALL_CENTERED);
#endif
prv_set_window_color(ACCEPT_COLOR, false);
prv_action_bar_setup(can_decline ? PhoneCallActions_Decline : PhoneCallActions_None);
prv_start_call_duration_timer();
prv_set_answer_window();
}
void phone_ui_handle_call_end(bool call_accepted, bool disconnected) {
if (!s_phone_ui_data) {
PBL_LOG(LOG_LEVEL_ERROR, "Can't handle call end, UI isn't setup");
return;
}
prv_stop_ringing();
prv_stop_call_duration_timer();
// This must be called before prv_set_status_text, otherwise the text will not be centered
prv_action_bar_setup(PhoneCallActions_None);
if (call_accepted) {
prv_set_icon_resource(TIMELINE_RESOURCE_DURING_PHONE_CALL);
prv_set_window_color(ACCEPT_COLOR, true);
prv_set_status_text(ACCEPTED);
} else {
prv_set_icon_resource(TIMELINE_RESOURCE_DISMISSED_PHONE_CALL);
prv_set_window_color(DECLINE_COLOR, true);
if (disconnected) {
prv_set_status_text(DISCONNECTED);
} else {
prv_set_status_text(DECLINED);
}
}
prv_window_pop_with_delay(CALL_END_DELAY_MS);
}
void phone_ui_handle_call_hide(void) {
// Just pop the window - it'll handle all the cleanup
prv_window_pop();
}
void phone_ui_handle_caller_id(PebblePhoneCaller *caller) {
if (!s_phone_ui_data) {
PBL_LOG(LOG_LEVEL_ERROR, "Can't update caller id, UI isn't setup");
return;
}
if (caller) {
prv_display_caller_info(caller);
}
}