/* * 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 "progress_window.h" #include "applib/applib_malloc.auto.h" #include "applib/fonts/fonts.h" #include "applib/ui/app_window_stack.h" #include "applib/ui/window_private.h" #include "applib/ui/window_stack.h" #include "kernel/pbl_malloc.h" #include "services/common/compositor/compositor_transitions.h" #include "services/normal/timeline/timeline_resources.h" #include "system/logging.h" #include "system/passert.h" #include "util/math.h" #define SCROLL_OUT_MS 250 #define BAR_HEIGHT PROGRESS_SUGGESTED_HEIGHT #define BAR_WIDTH 80 #define BAR_TO_TRANS_MS 160 #define TRANS_TO_DOT_MS 90 #define DOT_TRANSITION_RADIUS 13 #define DOT_COMPOSITOR_RADIUS 7 #define DOT_OFFSET 25 #define FAKE_PROGRESS_UPDATE_INTERVAL 200 #define FAKE_PROGRESS_UPDATE_AMOUNT 2 #define INITIAL_PERCENT 0 static void prv_finished(ProgressWindow *data, bool success) { if (data->callbacks.finished) { data->callbacks.finished(data, success, data->context); } } /////////////////////////////// // Animation Related Functions /////////////////////////////// static void prv_animation_stopped_success(Animation *animation, bool finished, void *context) { ProgressWindow *data = context; const bool success = true; prv_finished(data, success); } static void prv_finished_failure_callback(void *data) { const bool success = false; prv_finished(data, success); } static void prv_show_peek_layer(ProgressWindow *data) { if (data->is_peek_layer_used) { Layer *root_layer = window_get_root_layer(&data->window); PeekLayer *peek_layer = &data->peek_layer; peek_layer_play(peek_layer); layer_add_child(root_layer, (Layer *)peek_layer); const int standing_ms = 1 * MS_PER_SECOND; data->peek_layer_timer = evented_timer_register(PEEK_LAYER_UNFOLD_DURATION + standing_ms, false, prv_finished_failure_callback, data); } else { prv_finished_failure_callback(data); } } static void prv_animation_stopped_failure(Animation *animation, bool finished, void *context) { ProgressWindow *data = context; prv_show_peek_layer(data); } static void prv_schedule_progress_success_animation(ProgressWindow *data) { #if !PLATFORM_TINTIN GRect beg = data->progress_layer.layer.bounds; GRect mid = beg; GRect end = beg; // Morph from progress_layer to a large transition dot to the compositor dot // by changing the bounds of the progress_layer using 2 animations layer_set_clips((Layer *)&data->progress_layer, false); // Extending bounds to grow the dot progress_layer_set_corner_radius(&data->progress_layer, DOT_TRANSITION_RADIUS); mid.size.w = DOT_TRANSITION_RADIUS * 2; mid.size.h = DOT_TRANSITION_RADIUS * 2; mid.origin.x = DOT_OFFSET - DOT_TRANSITION_RADIUS + 2; mid.origin.y = BAR_HEIGHT - DOT_TRANSITION_RADIUS + 1; // shift to accommodate growing radius end.size.w = DOT_COMPOSITOR_RADIUS * 2; end.size.h = DOT_COMPOSITOR_RADIUS * 2; end.origin.x = DOT_OFFSET - DOT_COMPOSITOR_RADIUS - 1; end.origin.y = BAR_HEIGHT - DOT_COMPOSITOR_RADIUS - 2; // shift to accommodate growing radius PropertyAnimation *prop_anim = property_animation_create_layer_bounds((Layer *)&data->progress_layer, &beg, &mid); Animation *animation1 = property_animation_get_animation(prop_anim); animation_set_duration(animation1, BAR_TO_TRANS_MS); animation_set_curve(animation1, AnimationCurveEaseIn); prop_anim = property_animation_create_layer_bounds((Layer *)&data->progress_layer, &mid, &end); Animation *animation2 = property_animation_get_animation(prop_anim); animation_set_duration(animation2, TRANS_TO_DOT_MS); animation_set_curve(animation2, AnimationCurveLinear); animation_set_handlers(animation2, (AnimationHandlers) { .stopped = prv_animation_stopped_success, }, data); Animation *animation = animation_sequence_create(animation1, animation2, NULL); data->result_animation = animation; animation_schedule(animation); #else // Don't animate to a dot on old platforms, just finish immediately. static const bool success = true; prv_finished(data, success); #endif } static void prv_schedule_progress_failure_animation(ProgressWindow *data, uint32_t timeline_res_id, const char *message, uint32_t delay) { // Initialize the peek layer if (timeline_res_id || message) { Layer *root_layer = window_get_root_layer(&data->window); PeekLayer *peek_layer = &data->peek_layer; peek_layer_init(peek_layer, &root_layer->frame); peek_layer_set_title_font(peek_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD)); TimelineResourceInfo timeline_res = { .res_id = timeline_res_id, }; peek_layer_set_icon(peek_layer, &timeline_res); peek_layer_set_title(peek_layer, message); peek_layer_set_background_color(peek_layer, PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite)); data->is_peek_layer_used = true; } #if !PLATFORM_TINTIN // Animate the progress bar out, by shrinking it's width from it's current size down to 0. // When this completes, prv_animation_stopped_failure will show the peek layer. GRect *start = &data->progress_layer.layer.frame; GRect stop = *start; stop.size.w = 0; PropertyAnimation *prop_anim = property_animation_create_layer_frame((Layer *)&data->progress_layer, start, &stop); Animation *animation = property_animation_get_animation(prop_anim); // If we failed, pause on the screen for a little. animation_set_delay(animation, delay); animation_set_duration(animation, SCROLL_OUT_MS); animation_set_curve(animation, AnimationCurveEaseOut); animation_set_handlers(animation, (AnimationHandlers) { .stopped = prv_animation_stopped_failure, }, data); data->result_animation = animation; animation_schedule(animation); #else // Don't animate, just show the peek layer immediately. prv_show_peek_layer(data); #endif } //////////////////////////// // Internal Helper Functions //////////////////////////// //! Used to clean up the application's data before exiting static void prv_cancel_fake_progress_timer(ProgressWindow *data) { if (data->fake_progress_timer != EVENTED_TIMER_INVALID_ID) { evented_timer_cancel(data->fake_progress_timer); data->fake_progress_timer = EVENTED_TIMER_INVALID_ID; } } static void prv_set_progress(ProgressWindow *data, int16_t progress) { data->progress_percent = CLIP(progress, data->progress_percent, MAX_PROGRESS_PERCENT); progress_layer_set_progress(&data->progress_layer, data->progress_percent); } static void prv_fake_update_progress(void *context) { ProgressWindow *data = context; prv_set_progress(data, data->progress_percent + FAKE_PROGRESS_UPDATE_AMOUNT); if (data->progress_percent >= data->max_fake_progress_percent) { // Hit the max, we're done data->fake_progress_timer = EVENTED_TIMER_INVALID_ID; } else { data->fake_progress_timer = evented_timer_register(FAKE_PROGRESS_UPDATE_INTERVAL, false, prv_fake_update_progress, data); } } //////////////////////////// // Public API //////////////////////////// void progress_window_set_max_fake_progress(ProgressWindow *window, int16_t max_fake_progress_percent) { window->max_fake_progress_percent = CLIP(max_fake_progress_percent, 0, MAX_PROGRESS_PERCENT); } void progress_window_set_progress(ProgressWindow *window, int16_t progress) { if (window->state == ProgressWindowState_FakeProgress) { // We've seen our first bit of real progress, stop faking it. prv_cancel_fake_progress_timer(window); window->state = ProgressWindowState_RealProgress; } prv_set_progress(window, progress); } void progress_window_set_result_success(ProgressWindow *window) { if (window->state == ProgressWindowState_Result) { // Ignore requests to change the result once we already have one return; } window->state = ProgressWindowState_Result; prv_cancel_fake_progress_timer(window); prv_set_progress(window, MAX_PROGRESS_PERCENT); prv_schedule_progress_success_animation(window); } void progress_window_set_result_failure(ProgressWindow *window, uint32_t timeline_res, const char *message, uint32_t delay) { if (window->state == ProgressWindowState_Result) { // Ignore requests to change the result once we already have one return; } window->state = ProgressWindowState_Result; prv_cancel_fake_progress_timer(window); prv_schedule_progress_failure_animation(window, timeline_res, message, delay); } void progress_window_set_callbacks(ProgressWindow *window, ProgressWindowCallbacks callbacks, void *context) { window->context = context; window->callbacks = callbacks; } void progress_window_set_back_disabled(ProgressWindow *window, bool disabled) { window_set_overrides_back_button(&window->window, disabled); } void progress_window_push(ProgressWindow *window, WindowStack *window_stack) { const bool animated = true; window_stack_push(window_stack, (Window *)window, animated); } void app_progress_window_push(ProgressWindow *window) { const bool animated = true; app_window_stack_push((Window *)window, animated); } void progress_window_pop(ProgressWindow *window) { const bool animated = true; window_stack_remove((Window *)window, animated); } void progress_window_init(ProgressWindow *data) { // Create and set up the window Window *window = &data->window; window_init(window, WINDOW_NAME("Progress Window")); window_set_background_color(window, PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite)); const GRect *bounds = &window->layer.bounds; GPoint center = grect_center_point(bounds); const GRect progress_bounds = (GRect) { .origin = { center.x - (BAR_WIDTH / 2), center.y - (BAR_HEIGHT / 2) }, .size = { BAR_WIDTH, BAR_HEIGHT }, }; ProgressLayer *progress_layer = &data->progress_layer; progress_layer_init(progress_layer, &progress_bounds); #if PBL_COLOR progress_layer_set_foreground_color(progress_layer, GColorWhite); progress_layer_set_background_color(progress_layer, GColorBlack); #endif progress_layer_set_corner_radius(progress_layer, PROGRESS_SUGGESTED_CORNER_RADIUS); layer_add_child(&window->layer, (Layer *)progress_layer); data->max_fake_progress_percent = PROGRESS_WINDOW_DEFAULT_FAKE_PERCENT; data->state = ProgressWindowState_FakeProgress; data->is_peek_layer_used = false; data->fake_progress_timer = evented_timer_register(FAKE_PROGRESS_UPDATE_INTERVAL, false, prv_fake_update_progress, data); prv_set_progress(data, INITIAL_PERCENT); } void progress_window_deinit(ProgressWindow *data) { if (!data) { return; } animation_unschedule(data->result_animation); peek_layer_deinit(&data->peek_layer); data->is_peek_layer_used = false; prv_cancel_fake_progress_timer(data); if (data->peek_layer_timer != EVENTED_TIMER_INVALID_ID) { evented_timer_cancel(data->peek_layer_timer); data->peek_layer_timer = EVENTED_TIMER_INVALID_ID; } } ProgressWindow *progress_window_create(void) { ProgressWindow *window = applib_zalloc(sizeof(ProgressWindow)); progress_window_init(window); return window; } void progress_window_destroy(ProgressWindow *window) { if (window) { progress_window_pop(window); } progress_window_deinit(window); applib_free(window); }