Import of the watch repository from Pebble

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

View file

@ -0,0 +1,238 @@
/*
* 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 "weather_app.h"
#include "weather_app_layout.h"
#include "weather_app_splash_screen.h"
#include "weather_app_warning_dialog.h"
#include "applib/app.h"
#include "applib/event_service_client.h"
#include "applib/ui/click.h"
#include "applib/ui/content_indicator.h"
#include "applib/ui/ui.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "process_management/pebble_process_md.h"
#include "process_state/app_state/app_state.h"
#include "resource/resource_ids.auto.h"
#include "services/common/i18n/i18n.h"
#include "services/normal/timeline/timeline.h"
#include "services/normal/weather/weather_service.h"
#include "services/normal/weather/weather_types.h"
#include "util/array.h"
#include "util/attributes.h"
#include "util/list.h"
#include "util/math.h"
typedef struct WeatherAppData {
Window window;
WeatherAppLayout layout;
WeatherDataListNode *forecasts_list_head;
size_t forecasts_count;
unsigned int current_forecast_index;
EventServiceInfo weather_event_info;
WeatherAppWarningDialog *warning_dialog;
} WeatherAppData;
static bool prv_is_weather_forecast_recent(WeatherLocationForecast *forecast) {
if (!forecast) {
return false;
}
const time_t current_time_utc = rtc_get_time();
const int recent_threshold_seconds = 5 * SECONDS_PER_HOUR / 2; // 2.5 hours
const int seconds_since_forecast_was_updated = current_time_utc - forecast->time_updated_utc;
return (seconds_since_forecast_was_updated < recent_threshold_seconds);
}
static void prv_warning_dialog_dismiss_cb(void) {
WeatherAppData *data = app_state_get_user_data();
data->warning_dialog = NULL;
}
static void prv_show_warning_dialog(WeatherAppData *data, bool exit_on_pop,
const char *localized_text) {
if (data->warning_dialog) {
return; // only show one dialog at a time
}
if (exit_on_pop) {
bool animated = false;
app_window_stack_pop_all(animated);
}
data->warning_dialog = weather_app_warning_dialog_push(localized_text,
prv_warning_dialog_dismiss_cb);
}
static void prv_handle_weather(PebbleEvent *unused_event, void *unused_context) {
// Unschedule any ongoing animations that would try to touch the weather data we're about to
// update
animation_unschedule_all();
size_t forecasts_count_out = 0;
WeatherDataListNode *forecasts_list_head =
weather_service_locations_list_create(&forecasts_count_out);
WeatherAppData *data = app_state_get_user_data();
weather_service_locations_list_destroy(data->forecasts_list_head);
WeatherAppLayout *layout = &data->layout;
if (forecasts_count_out > 0) {
weather_app_layout_set_data(layout, &forecasts_list_head->forecast);
const bool multiple_forecasts_exist = (forecasts_count_out > 1);
weather_app_layout_set_down_arrow_visible(layout, multiple_forecasts_exist);
data->forecasts_list_head = forecasts_list_head;
// Only show the first forecast if the number of forecasts has differed between fetches.
// i.e. assume that the same number of forecasts means the locations have remained the same.
if (data->forecasts_count != forecasts_count_out) {
data->forecasts_count = forecasts_count_out;
data->current_forecast_index = 0;
}
} else {
/// Shown when there are no forecasts available to show the user
const char *warning_text = i18n_get("No location information available. To see weather, add "\
"locations in your Pebble mobile app.", data);
const bool exit_on_pop = true;
prv_show_warning_dialog(data, exit_on_pop, warning_text);
weather_app_layout_set_down_arrow_visible(layout, false);
weather_app_layout_set_data(layout, NULL);
}
}
static void prv_main_window_appear(Window *window) {
WeatherAppData *data = app_state_get_user_data();
data->weather_event_info = (EventServiceInfo) {
.type = PEBBLE_WEATHER_EVENT,
.handler = prv_handle_weather,
};
event_service_client_subscribe(&data->weather_event_info);
}
static void prv_main_window_load(Window *window) {
WeatherAppData *data = app_state_get_user_data();
layer_add_child(&window->layer, &data->layout.root_layer);
}
static void prv_main_window_disappear(Window *window) {
WeatherAppData *data = app_state_get_user_data();
event_service_client_unsubscribe(&data->weather_event_info);
}
static void prv_up_down_click_handler(ClickRecognizerRef recognizer, void *context) {
WeatherAppData *data = app_state_get_user_data();
const bool not_enough_items_to_scroll = (data->forecasts_count <= 1);
if (not_enough_items_to_scroll) {
return;
}
const bool is_down_pressed = (click_recognizer_get_button_id(recognizer) == BUTTON_ID_DOWN);
const int delta = is_down_pressed ? 1 : -1;
data->current_forecast_index = positive_modulo(data->current_forecast_index + delta,
data->forecasts_count);
WeatherDataListNode *node =
weather_service_locations_list_get_location_at_index(data->forecasts_list_head,
data->current_forecast_index);
weather_app_layout_animate(&data->layout, &node->forecast, is_down_pressed);
}
static void prv_main_window_click_provider(void *context) {
window_single_click_subscribe(BUTTON_ID_UP, prv_up_down_click_handler);
window_single_click_subscribe(BUTTON_ID_DOWN, prv_up_down_click_handler);
}
static void prv_main_window_unload(Window *window) {
WeatherAppData *data = app_state_get_user_data();
weather_app_layout_deinit(&data->layout);
}
static NOINLINE void prv_init(void) {
WeatherAppData *data = app_zalloc_check(sizeof(WeatherAppData));
app_state_set_user_data(data);
Window *window = &data->window;
window_init(window, WINDOW_NAME("Weather"));
const WindowHandlers window_handlers = {
.appear = prv_main_window_appear,
.load = prv_main_window_load,
.disappear = prv_main_window_disappear,
.unload = prv_main_window_unload,
};
window_set_window_handlers(window, &window_handlers);
window_set_click_config_provider(window, prv_main_window_click_provider);
window_set_user_data(window, data);
const GRect *layout_frame = &window->layer.bounds;
WeatherAppLayout *layout = &data->layout;
weather_app_layout_init(layout, layout_frame);
window_set_user_data(window, layout);
// Fetch initial data
prv_handle_weather(NULL, NULL);
if (data->forecasts_count == 0) {
return;
}
const bool animated = true;
app_window_stack_push(&data->window, animated);
// Request the default forecast separately instead of using the forecast list in `data` to avoid
// any potential race conditions
WeatherLocationForecast *default_forecast = weather_service_create_default_forecast();
const bool is_default_forecast_data_recent = prv_is_weather_forecast_recent(default_forecast);
weather_service_destroy_default_forecast(default_forecast);
// TODO PBL-38484: Consider using a different dialog for when data is stale but phone is connected
if (is_default_forecast_data_recent || connection_service_peek_pebble_app_connection()) {
const uint32_t splash_screen_timeout_ms = 500;
weather_app_splash_screen_push(splash_screen_timeout_ms);
} else {
/// Shown when there is no connection to the phone and the data that we have is not recent
const char *warning_text = i18n_get("Unable to connect. Your weather data may be out of date; "\
"try checking the connection on your phone.", data);
const bool exit_on_pop = false;
prv_show_warning_dialog(data, exit_on_pop, warning_text);
}
}
static void prv_deinit(void) {
WeatherAppData *data = app_state_get_user_data();
i18n_free_all(data);
}
static void prv_main(void) {
prv_init();
app_event_loop();
prv_deinit();
}
const PebbleProcessMd* weather_app_get_info() {
const bool is_visible_in_launcher = weather_service_supported_by_phone();
static const PebbleProcessMdSystem s_weather_app_info = {
.common = {
.main_func = prv_main,
.uuid = UUID_WEATHER_DATA_SOURCE,
},
.name = i18n_noop("Weather"),
#if CAPABILITY_HAS_APP_GLANCES
.icon_resource_id = RESOURCE_ID_GENERIC_WEATHER_TINY,
#endif
};
return is_visible_in_launcher ? (const PebbleProcessMd *)&s_weather_app_info : NULL;
}

View file

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

View file

@ -0,0 +1,556 @@
/*
* 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 "weather_app_layout.h"
#include "applib/fonts/fonts.h"
#include "applib/graphics/gcontext.h"
#include "applib/graphics/gdraw_command_image.h"
#include "applib/graphics/gpath.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/graphics_circle.h"
#include "applib/graphics/gtypes.h"
#include "applib/graphics/text.h"
#include "applib/ui/animation.h"
#include "applib/ui/animation_interpolate.h"
#include "applib/ui/animation_timing.h"
#include "applib/ui/content_indicator.h"
#include "applib/ui/kino/kino_layer.h"
#include "applib/ui/kino/kino_reel/morph_square.h"
#include "applib/ui/kino/kino_reel/transform.h"
#include "applib/ui/window.h"
#include "apps/system_apps/timeline/text_node.h"
#include "font_resource_keys.auto.h"
#include "kernel/pbl_malloc.h"
#include "resource/resource_ids.auto.h"
#include "services/common/i18n/i18n.h"
#include "services/normal/timeline/timeline_resources.h"
#include "services/normal/weather/weather_service.h"
#include "services/normal/weather/weather_types.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/size.h"
#include "util/trig.h"
#include <stdio.h>
#define WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT (18)
#define WEATHER_APP_LAYOUT_TOP_PADDING PBL_IF_RECT_ELSE(0, 15)
#define WEATHER_APP_LAYOUT_TIMELINE_ICON_RESOURCE_SIZE (TimelineResourceSizeTiny)
#define WEATHER_APP_LAYOUT_CONTENT_LAYER_HORIZONTAL_INSET PBL_IF_RECT_ELSE(3, 23)
static int prv_draw_text(GPoint offset, int max_width, GContext *context,
const char *text, const GFont font,
GColor font_color, GTextAlignment alignment) {
const int height = fonts_get_font_height(font);
const GRect box = (GRect) {offset, GSize(max_width, height)};
graphics_context_set_text_color(context, font_color);
graphics_draw_text(context, text, font, box, GTextOverflowModeFill, alignment, NULL);
return height;
}
static void prv_draw_weather_background(const GRect *circle_bounding_box, GContext *context,
GColor background_color) {
if (!gcolor_is_invisible(background_color)) {
graphics_context_set_fill_color(context, background_color);
graphics_fill_oval(context, *circle_bounding_box, GOvalScaleModeFitCircle);
}
}
static void prv_fill_high_low_temp_buffer(const int high, const int low, char *buffer,
const size_t buffer_size, const void *i18n_owner) {
if ((high == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP) &&
(low == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP)) {
/// Shown when neither high nor low temperature is known
const char *both_temps_no_data = i18n_get("--° / --°", i18n_owner);
strncpy(buffer, both_temps_no_data, strlen(both_temps_no_data));
buffer[buffer_size - 1] = '\0';
} else if (low == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP) {
/// Shown when only the day's high temperature is known, (e.g. "68° / --°")
snprintf(buffer, buffer_size, i18n_get("%i° / --°", i18n_owner), high);
} else if (high == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP) {
/// Shown when only the day's low temperature is known (e.g. "--° / 52°")
snprintf(buffer, buffer_size, i18n_get("--° / %i°", i18n_owner), low);
} else {
/// A day's high and low temperature, separated by a foward slash (e.g. "68° / 52°")
snprintf(buffer, buffer_size, i18n_get("%i° / %i°", i18n_owner), high, low);
}
}
#define GPS_ARROW_WIDTH (12)
#define GPS_ARROW_HEIGHT (14)
static const GPoint s_gps_arrow_path_points[] = {
{0, GPS_ARROW_HEIGHT},
{(GPS_ARROW_WIDTH / 2), 0},
{GPS_ARROW_WIDTH, GPS_ARROW_HEIGHT},
// This 6/7 height ratio for the arrow notch achieves the design spec
{(GPS_ARROW_WIDTH / 2), (GPS_ARROW_HEIGHT * 6 / 7)}
};
static void prv_draw_gps_arrow_node_callback(GContext *ctx, const GRect *rect,
UNUSED const GTextNodeDrawConfig *config, bool render,
GSize *size_out, UNUSED void *user_data) {
GPath gps_arrow_path = (GPath) {
.num_points = ARRAY_LENGTH(s_gps_arrow_path_points),
.points = (GPoint *)s_gps_arrow_path_points,
.offset = rect->origin,
// Ideal rotation would be 45 degrees, but the shape of the arrow matches the design best at
// 38 degrees
.rotation = DEG_TO_TRIGANGLE(38),
};
if (render) {
graphics_context_set_fill_color(ctx, GColorBlack);
gpath_draw_filled(ctx, &gps_arrow_path);
}
if (size_out) {
// Note that gpath_outer_rect() doesn't take into account the rotation; we'll add margin to the
// location text node to account for it
*size_out = gpath_outer_rect(&gps_arrow_path).size;
}
}
static GTextNode *prv_create_location_name_area_node(const WeatherLocationForecast *forecast,
GFont location_font) {
const GTextAlignment location_name_alignment = PBL_IF_RECT_ELSE(GTextAlignmentLeft,
GTextAlignmentCenter);
// One node for the location name text and one node for the possible GPS arrow
const size_t max_nodes = 2;
GTextNodeHorizontal *horizontal_node = graphics_text_node_create_horizontal(max_nodes);
horizontal_node->horizontal_alignment = location_name_alignment;
GTextNodeText *location_text_node = graphics_text_node_create_text(0);
location_text_node->text = forecast->location_name;
location_text_node->font = location_font;
location_text_node->color = GColorBlack;
location_text_node->overflow = GTextOverflowModeTrailingEllipsis;
if (forecast->is_current_location) {
// Horizontal spacing between location name and GPS arrow is spec'd by design to be 11 pixels
location_text_node->node.margin = GSize(11, 0);
}
graphics_text_node_container_add_child(&horizontal_node->container, &location_text_node->node);
if (forecast->is_current_location) {
GTextNodeCustom *arrow_node = graphics_text_node_create_custom(prv_draw_gps_arrow_node_callback,
NULL);
arrow_node->node.offset =
GPoint(0, (int16_t)(fonts_get_font_cap_offset(location_font) / 2));
graphics_text_node_container_add_child(&horizontal_node->container, &arrow_node->node);
}
return &horizontal_node->container.node;
}
static GSize prv_draw_location_name_area(GPoint offset, int max_width, GContext *ctx,
GFont location_font,
const WeatherLocationForecast *forecast) {
GTextNode *location_name_area_node = prv_create_location_name_area_node(forecast, location_font);
GRect location_name_area_rect = (GRect) {
.origin = offset,
.size = GSize(max_width, fonts_get_font_height(location_font)),
};
#if PBL_ROUND
// On round the location name text and arrow can be obscured by the edges of the bezel, so we
// horizontally inset the rectangle by a few pixels
const int16_t horizontal_inset = 5;
location_name_area_rect = grect_inset(location_name_area_rect,
GEdgeInsets(0, horizontal_inset, 0));
#endif
GSize location_name_area_size;
graphics_text_node_draw(location_name_area_node, ctx, &location_name_area_rect, NULL,
&location_name_area_size);
graphics_text_node_destroy(location_name_area_node);
return location_name_area_size;
}
// All text before the separator
static void prv_draw_top_half_text(const WeatherAppLayout *layout, GPoint *current_offset,
int content_width,
GContext *context) {
const WeatherLocationForecast *forecast = layout->forecast;
current_offset->y += prv_draw_location_name_area(*current_offset, content_width, context,
layout->location_font, forecast).h;
const int location_and_today_temperature_vertical_spacing = 7;
current_offset->y += location_and_today_temperature_vertical_spacing;
char text_buffer[15] = {0};
const size_t max_text_buff_size = ARRAY_LENGTH(text_buffer);
if (forecast->current_temp == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP) {
/// Shown when today's current temperature is unknown
const char *unknown_temp_string = i18n_get("--°", layout);
strncpy(text_buffer, unknown_temp_string, strlen(unknown_temp_string));
text_buffer[max_text_buff_size - 1] = '\0';
} else {
/// Today's current temperature (e.g. "68°")
snprintf(text_buffer, max_text_buff_size, i18n_get("%i°", layout), forecast->current_temp);
}
current_offset->y += prv_draw_text(*current_offset, content_width, context, text_buffer,
layout->temperature_font, GColorBlack, GTextAlignmentLeft);
prv_fill_high_low_temp_buffer(forecast->today_high, forecast->today_low, text_buffer,
max_text_buff_size, layout);
current_offset->y += prv_draw_text(*current_offset, content_width, context, text_buffer,
layout->high_low_phrase_font, GColorBlack,
GTextAlignmentLeft);
const int today_high_low_gap_vertical_spacing_reduction = 2;
current_offset->y -= today_high_low_gap_vertical_spacing_reduction;
current_offset->y += prv_draw_text(*current_offset, content_width, context,
forecast->current_weather_phrase, layout->high_low_phrase_font,
GColorBlack, GTextAlignmentLeft);
}
// All text after the separator
static void prv_draw_bottom_half_text(const WeatherAppLayout *layout, GPoint *current_offset,
int content_width, GContext *context) {
const WeatherLocationForecast *forecast = layout->forecast;
const int separator_tomorrow_title_vertical_spacing = 6;
current_offset->y += separator_tomorrow_title_vertical_spacing;
current_offset->y += prv_draw_text(*current_offset, content_width, context,
/// Refers to the weather conditions for tomorrow
i18n_get("TOMORROW", layout), layout->tomorrow_font,
GColorBlack, GTextAlignmentLeft);
char text_buffer[15] = {0};
const size_t max_text_buff_size = ARRAY_LENGTH(text_buffer);
prv_fill_high_low_temp_buffer(forecast->tomorrow_high, forecast->tomorrow_low, text_buffer,
max_text_buff_size, layout);
prv_draw_text(*current_offset, content_width, context, text_buffer, layout->high_low_phrase_font,
GColorBlack, GTextAlignmentLeft);
}
static void prv_draw_weather_icon_backgrounds(const WeatherAppLayout *layout,
const GRect *content_bounds, GContext *context) {
const WeatherLocationForecast *forecast = layout->forecast;
// assume that both current and tomorrow weather icons are the same size
const GSize icon_size = layout->current_weather_icon_layer.layer.bounds.size;
const unsigned int weather_icon_bg_circle_diam = integer_sqrt(2 * icon_size.w * icon_size.h);
const int today_weather_bg_circle_top_margin = 28;
GRect bg_circle_bounding_box = GRect(grect_get_max_x(content_bounds) -
weather_icon_bg_circle_diam,
today_weather_bg_circle_top_margin,
weather_icon_bg_circle_diam, weather_icon_bg_circle_diam);
prv_draw_weather_background(&bg_circle_bounding_box, context,
weather_type_get_bg_color(forecast->current_weather_type));
const int weather_bg_circle_vertical_spacing = 40;
bg_circle_bounding_box.origin.y += weather_icon_bg_circle_diam +
weather_bg_circle_vertical_spacing;
prv_draw_weather_background(&bg_circle_bounding_box, context,
weather_type_get_bg_color(forecast->tomorrow_weather_type));
}
static void prv_render_layout(Layer *layer, GContext *context) {
// "Content" refers to everything except the dot separator
const GRect content_bounds =
grect_inset(layer->bounds, GEdgeInsets(0, WEATHER_APP_LAYOUT_CONTENT_LAYER_HORIZONTAL_INSET,
0));
const int content_x_offset = content_bounds.origin.x;
const int content_width = content_bounds.size.w;
const WeatherAppLayout *layout = window_get_user_data(layer_get_window(layer));
const WeatherLocationForecast *forecast = layout->forecast;
if (!forecast) {
// Nothing to draw.
return;
}
// start at 1 from the top to match design docs
GPoint current_offset = GPoint(content_x_offset, 1);
GPoint *offset = &current_offset;
prv_draw_top_half_text(layout, offset, content_width, context);
// dotted separator
const int phrase_separator_vertical_spacing = 10;
current_offset.y += phrase_separator_vertical_spacing;
const GPoint separator_start = GPoint(0, current_offset.y);
graphics_context_set_stroke_width(context, 5);
graphics_context_set_stroke_color(context, PBL_IF_COLOR_ELSE(GColorLightGray, GColorBlack));
graphics_draw_horizontal_line_dotted(context, separator_start,
layer->bounds.size.w);
if (!layout->animation_state.hide_bottom_half_text) {
prv_draw_bottom_half_text(layout, offset, content_width, context);
}
prv_draw_weather_icon_backgrounds(layout, &content_bounds, context);
}
static void prv_content_indicator_setup_direction(ContentIndicator *content_indicator,
Layer *indicator_layer,
ContentIndicatorDirection direction) {
content_indicator_configure_direction(content_indicator, direction, &(ContentIndicatorConfig) {
.layer = indicator_layer,
.colors.foreground = GColorBlack,
.colors.background = GColorLightGray,
});
}
void weather_app_layout_init(WeatherAppLayout *layout, const GRect *frame) {
layout->location_font = fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD);
layout->temperature_font = fonts_get_system_font(FONT_KEY_LECO_26_BOLD_NUMBERS_AM_PM);
layout->high_low_phrase_font = fonts_get_system_font(FONT_KEY_GOTHIC_18);
layout->tomorrow_font = fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD);
Layer *root_layer = &layout->root_layer;
layer_init(root_layer, frame);
Layer *down_arrow_layer = &layout->down_arrow_layer;
const GRect down_arrow_layer_frame = grect_inset(
*frame, GEdgeInsets(frame->size.h - WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT, 0, 0));
layer_init(down_arrow_layer, &down_arrow_layer_frame);
layer_add_child(root_layer, down_arrow_layer);
const int content_layer_side_padding = PBL_IF_RECT_ELSE(5, 12);
const GRect content_layer_frame = grect_inset(*frame,
GEdgeInsets(WEATHER_APP_LAYOUT_TOP_PADDING,
content_layer_side_padding,
WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT));
Layer *content_layer = &layout->content_layer;
layer_init(content_layer, &content_layer_frame);
layer_set_update_proc(content_layer, prv_render_layout);
layer_add_child(root_layer, content_layer);
ContentIndicator *content_indicator = &layout->content_indicator;
content_indicator_init(content_indicator);
prv_content_indicator_setup_direction(content_indicator, down_arrow_layer,
ContentIndicatorDirectionDown);
const GSize icon_size =
timeline_resources_get_gsize(WEATHER_APP_LAYOUT_TIMELINE_ICON_RESOURCE_SIZE);
const int icon_layer_margin_top = PBL_IF_RECT_ELSE(33, 18);
const int icon_layer_right_margin = 5;
GRect icon_layer_frame = (GRect) {
{
content_layer_frame.size.w - icon_size.w - WEATHER_APP_LAYOUT_CONTENT_LAYER_HORIZONTAL_INSET -
icon_layer_right_margin,
content_layer_frame.origin.y + icon_layer_margin_top
},
icon_size
};
KinoLayer *current_weather_icon_layer = &layout->current_weather_icon_layer;
kino_layer_init(current_weather_icon_layer, &icon_layer_frame);
layer_add_child(content_layer, kino_layer_get_layer(current_weather_icon_layer));
const int icon_layer_spacing = 50;
icon_layer_frame.origin.y += icon_size.h + icon_layer_spacing;
KinoLayer *tomorrow_weather_icon_layer = &layout->tomorrow_weather_icon_layer;
kino_layer_init(tomorrow_weather_icon_layer, &icon_layer_frame);
layer_add_child(content_layer, kino_layer_get_layer(tomorrow_weather_icon_layer));
}
static uint32_t prv_get_resource_id_for_weather_type(WeatherType type) {
const TimelineResourceInfo timeline_res = {
.res_id = weather_type_get_timeline_resource_id(type),
};
AppResourceInfo icon_res_info;
timeline_resources_get_id(&timeline_res, WEATHER_APP_LAYOUT_TIMELINE_ICON_RESOURCE_SIZE,
&icon_res_info);
return icon_res_info.res_id;
}
void weather_app_layout_set_data(WeatherAppLayout *layout,
const WeatherLocationForecast *forecast) {
layout->forecast = forecast;
const uint32_t current_weather_res_id = forecast ?
prv_get_resource_id_for_weather_type(forecast->current_weather_type) : RESOURCE_ID_INVALID;
const uint32_t tomorrow_weather_res_id = forecast ?
prv_get_resource_id_for_weather_type(forecast->tomorrow_weather_type) : RESOURCE_ID_INVALID;
kino_layer_set_reel_with_resource(&layout->current_weather_icon_layer, current_weather_res_id);
kino_layer_set_reel_with_resource(&layout->tomorrow_weather_icon_layer, tomorrow_weather_res_id);
layer_mark_dirty(&layout->root_layer);
}
void weather_app_layout_set_down_arrow_visible(WeatherAppLayout *layout, bool is_down_visible) {
content_indicator_set_content_available(&layout->content_indicator, ContentIndicatorDirectionDown,
is_down_visible);
}
void weather_app_layout_deinit(WeatherAppLayout *layout) {
i18n_free_all(layout);
layer_deinit(&layout->root_layer);
}
// Down arrow layer grows until a point, after which the entire content teleports to a height
// slightly higher than its resting position, then relaxes into place
static void prv_down_animation_update(Animation *animation, AnimationProgress normalized) {
WeatherAppLayout *layout = animation_get_context(animation);
// Progress at which to switch from the down arrow growing to entire content relaxing downwards
const AnimationProgress animation_cut_frame_progress =
(interpolate_moook_in_duration() * ANIMATION_NORMALIZED_MAX) / interpolate_moook_duration();
// Progress at which to hide "TOMORROW" and tomorrow high / low temperature text
const AnimationProgress animation_hide_bottom_half_text_progress =
(animation_cut_frame_progress * 2) / 3;
int down_arrow_layer_height = WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT;
layout->animation_state.hide_bottom_half_text = false;
if (normalized <= animation_cut_frame_progress) {
if (normalized >= animation_hide_bottom_half_text_progress) {
layout->animation_state.hide_bottom_half_text = true;
}
// renormalize the progress so that interpolate_moook_in_only works as expected
int32_t new_normalized = animation_timing_scaled(normalized, ANIMATION_NORMALIZED_MIN,
animation_cut_frame_progress);
const int additional_down_arrow_height = 25;
// grow the down arrow layer
down_arrow_layer_height += interpolate_moook_in_only(new_normalized, 0,
additional_down_arrow_height);
} else {
// We've cut, so display the next forecast's data
if (layout->animation_state.next_forecast) {
weather_app_layout_set_data(layout, layout->animation_state.next_forecast);
layout->animation_state.next_forecast = NULL;
}
int32_t new_normalized = animation_timing_scaled(normalized, animation_cut_frame_progress,
ANIMATION_NORMALIZED_MAX);
// Relax the content by changing its top margin
const int animation_margin_top_from = WEATHER_APP_LAYOUT_TOP_PADDING - PBL_IF_RECT_ELSE(10, 15);
const int animation_margin_top_to = WEATHER_APP_LAYOUT_TOP_PADDING;
const int num_frames_from = 1;
const bool bounce_back = false;
int animation_margin_top = interpolate_moook_out(new_normalized, animation_margin_top_from,
animation_margin_top_to, num_frames_from,
bounce_back);
layout->content_layer.frame.origin.y = animation_margin_top;
// The down arrow's height follows the content margin. It starts off large, then goes back to
// its original size, as the content relaxes into place
down_arrow_layer_height += (-animation_margin_top + WEATHER_APP_LAYOUT_TOP_PADDING);
}
const GRect down_arrow_layer_frame = grect_inset(layout->root_layer.frame,
GEdgeInsets(layout->root_layer.frame.size.h - down_arrow_layer_height, 0, 0));
layer_set_frame(&layout->down_arrow_layer, &down_arrow_layer_frame);
layer_mark_dirty(&layout->root_layer);
}
// moves the entire root layer up back into place
static void prv_up_animation_update(Animation *animation, AnimationProgress normalized) {
const int root_layer_top_margin_from = (WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT * 2) / 3;
const int root_layer_top_margin_to = 0;
const int num_frames_from = 1;
const bool bounce_back = false;
int root_layer_top_margin = interpolate_moook_out(normalized, root_layer_top_margin_from,
root_layer_top_margin_to, num_frames_from, bounce_back);
WeatherAppLayout *layout = animation_get_context(animation);
if (layout->animation_state.next_forecast) {
layout->forecast = layout->animation_state.next_forecast;
layout->animation_state.next_forecast = NULL;
}
layout->root_layer.frame.origin.y = root_layer_top_margin;
layer_set_frame(&layout->root_layer, &layout->root_layer.frame);
}
static void prv_animation_stopped(Animation *animation, bool finished, void *context) {
WeatherAppLayout *layout = context;
if (layout->animation_state.next_forecast) {
weather_app_layout_set_data(layout, layout->animation_state.next_forecast);
layout->animation_state.next_forecast = NULL;
}
layout->animation_state.hide_bottom_half_text = false;
GRect *root_layer_frame = &layout->root_layer.frame;
root_layer_frame->origin.y = 0;
layer_set_frame(&layout->root_layer, root_layer_frame);
GRect *content_layer_frame = &layout->content_layer.frame;
content_layer_frame->origin.y = WEATHER_APP_LAYOUT_TOP_PADDING;
layer_set_frame(&layout->content_layer, content_layer_frame);
const GRect down_arrow_layer_frame = grect_inset(*root_layer_frame,
GEdgeInsets(root_layer_frame->size.h - WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT, 0, 0));
layer_set_frame(&layout->down_arrow_layer, &down_arrow_layer_frame);
}
static const AnimationImplementation s_down_animation_implementation = {
.update = &prv_down_animation_update,
};
static const AnimationImplementation s_up_animation_implementation = {
.update = &prv_up_animation_update,
};
static const AnimationHandlers s_animation_handlers = {
.stopped = &prv_animation_stopped,
};
static void prv_morph_weather_icons(KinoLayer *icon_layer, WeatherType from, WeatherType to,
uint32_t duration) {
uint32_t from_image_res_id =
prv_get_resource_id_for_weather_type(from);
KinoReel *from_reel = kino_reel_create_with_resource(from_image_res_id);
KinoReel *to_reel = kino_reel_create_with_resource(prv_get_resource_id_for_weather_type(to));
KinoReel *icon_reel = kino_reel_morph_square_create(from_reel, true);
kino_reel_transform_set_to_reel(icon_reel, to_reel, true);
kino_reel_transform_set_transform_duration(icon_reel, duration);
kino_layer_set_reel(icon_layer, icon_reel, true);
kino_layer_play(icon_layer);
}
void weather_app_layout_animate(WeatherAppLayout *layout, WeatherLocationForecast *new_forecast,
bool animate_down) {
animation_unschedule_all();
const uint32_t anim_duration = animate_down ? interpolate_moook_duration() :
interpolate_moook_out_duration();
layout->animation_state.next_forecast = new_forecast;
Animation *animation = animation_create();
animation_set_duration(animation, anim_duration);
InterpolateInt64Function interpolation = animate_down ? interpolate_moook :
interpolate_moook_in_only;
animation_set_custom_interpolation(animation, interpolation);
animation_set_handlers(animation, s_animation_handlers, layout);
const AnimationImplementation *implementation = animate_down ? &s_down_animation_implementation :
&s_up_animation_implementation;
animation_set_implementation(animation, implementation);
animation_schedule(animation);
prv_morph_weather_icons(&layout->current_weather_icon_layer,
layout->forecast->current_weather_type,
new_forecast->current_weather_type, anim_duration);
prv_morph_weather_icons(&layout->tomorrow_weather_icon_layer,
layout->forecast->tomorrow_weather_type,
new_forecast->tomorrow_weather_type, anim_duration);
}

View file

@ -0,0 +1,52 @@
/*
* 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/gdraw_command_image.h"
#include "applib/ui/content_indicator_private.h"
#include "applib/ui/kino/kino_layer.h"
#include "services/normal/weather/weather_service.h"
typedef struct WeatherAppLayout {
Layer root_layer;
Layer content_layer;
KinoLayer current_weather_icon_layer;
KinoLayer tomorrow_weather_icon_layer;
const WeatherLocationForecast *forecast;
GFont location_font;
GFont temperature_font;
GFont high_low_phrase_font;
GFont tomorrow_font;
Layer down_arrow_layer;
ContentIndicator content_indicator;
struct { // used during animations
const WeatherLocationForecast *next_forecast;
bool hide_bottom_half_text;
} animation_state;
} WeatherAppLayout;
void weather_app_layout_init(WeatherAppLayout *layout, const GRect *frame);
void weather_app_layout_set_data(WeatherAppLayout *layout,
const WeatherLocationForecast *forecast);
void weather_app_layout_set_down_arrow_visible(WeatherAppLayout *layout, bool is_down_visible);
void weather_app_layout_deinit(WeatherAppLayout *layout);
void weather_app_layout_animate(WeatherAppLayout *layout, WeatherLocationForecast *new_forecast,
bool animate_down);

View file

@ -0,0 +1,85 @@
/*
* 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 "weather_app_splash_screen.h"
#include "applib/app.h"
#include "applib/ui/kino/kino_layer.h"
#include "applib/ui/ui.h"
#include "kernel/pbl_malloc.h"
#include "resource/resource_ids.auto.h"
typedef struct SplashScreenData {
Window window;
KinoLayer logo_layer;
AppTimer *timer;
uint32_t timeout_ms;
} SplashScreenData;
static void prv_splash_screen_finished_callback(void *cb_data) {
SplashScreenData *data = cb_data;
data->timer = NULL;
const bool animated = false;
app_window_stack_remove(&data->window, animated);
}
static void prv_window_unload(Window *window) {
SplashScreenData *data = window_get_user_data(window);
kino_layer_deinit(&data->logo_layer);
// Execute conditional only if the user presses back while the splash screen is showing
if (data->timer) {
app_timer_cancel(data->timer);
const bool animated = true;
app_window_stack_pop_all(animated);
}
app_free(data);
}
static void prv_window_load(Window *window) {
SplashScreenData *data = window_get_user_data(window);
Layer *window_root_layer = &window->layer;
KinoLayer *logo_layer = &data->logo_layer;
kino_layer_init(logo_layer, &window_root_layer->bounds);
kino_layer_set_reel_with_resource(logo_layer,
RESOURCE_ID_WEATHER_CHANNEL_LOGO);
layer_add_child(window_root_layer,
kino_layer_get_layer(logo_layer));
data->timer = app_timer_register(data->timeout_ms,
prv_splash_screen_finished_callback,
data);
}
void weather_app_splash_screen_push(uint32_t timeout_ms) {
SplashScreenData *data = app_zalloc_check(sizeof(SplashScreenData));
data->timeout_ms = timeout_ms;
Window *window = &data->window;
window_init(window, WINDOW_NAME("Weather - Splash Screen"));
const GColor background_color = PBL_IF_COLOR_ELSE(GColorBlue, GColorBlack);
window_set_background_color(window, background_color);
const WindowHandlers window_handlers = {
.load = prv_window_load,
.unload = prv_window_unload,
};
window_set_window_handlers(window, &window_handlers);
window_set_user_data(window, data);
const bool animated = false;
app_window_stack_push(window, animated);
}

View file

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

View file

@ -0,0 +1,63 @@
/*
* 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 "weather_app_warning_dialog.h"
#include "applib/ui/app_window_stack.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/dialogs/expandable_dialog.h"
#include "kernel/pbl_malloc.h"
typedef struct WeatherAppWarningDialogData {
WeatherAppWarningDialogDismissedCallback dismissed_cb;
} WeatherAppWarningDialogData;
static void prv_warning_dialog_unload(void *context) {
WeatherAppWarningDialogData *data = context;
if (data->dismissed_cb) {
data->dismissed_cb();
}
task_free(data);
}
static void prv_warning_dialog_select_handler(ClickRecognizerRef recognizer, void *context) {
ExpandableDialog *expandable_dialog = context;
expandable_dialog_pop(expandable_dialog);
}
WeatherAppWarningDialog *weather_app_warning_dialog_push(const char *localized_string,
WeatherAppWarningDialogDismissedCallback dismissed_cb) {
WeatherAppWarningDialogData *data = task_zalloc_check(sizeof(WeatherAppWarningDialogData));
ExpandableDialog *expandable_dialog = expandable_dialog_create("Weather - warning dialog");
Dialog *dialog = expandable_dialog_get_dialog(expandable_dialog);
dialog_set_destroy_on_pop(dialog, false);
dialog_set_icon(dialog, RESOURCE_ID_GENERIC_WARNING_TINY);
dialog_set_text(dialog, localized_string);
const DialogCallbacks callbacks = {
.unload = prv_warning_dialog_unload,
};
dialog_set_callbacks(dialog, &callbacks, data);
expandable_dialog_show_action_bar(expandable_dialog, true);
expandable_dialog_set_select_action(expandable_dialog, RESOURCE_ID_ACTION_BAR_ICON_CHECK,
prv_warning_dialog_select_handler);
data->dismissed_cb = dismissed_cb;
app_expandable_dialog_push(expandable_dialog);
return expandable_dialog;
}

View file

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