/* * 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 "app_fetch_ui.h" #include #include #include #include "applib/app.h" #include "applib/fonts/fonts.h" #include "applib/ui/app_window_stack.h" #include "applib/ui/progress_window.h" #include "applib/ui/ui.h" #include "drivers/battery.h" #include "kernel/pbl_malloc.h" #include "process_management/app_install_manager.h" #include "process_management/app_manager.h" #include "process_management/worker_manager.h" #include "process_state/app_state/app_state.h" #include "services/common/i18n/i18n.h" #include "services/normal/app_fetch_endpoint.h" #include "services/normal/timeline/timeline_resources.h" #include "apps/system_apps/timeline/peek_layer.h" #include "shell/normal/watchface.h" #include "shell/shell.h" #include "shell/system_app_state_machine.h" #include "services/common/compositor/compositor_transitions.h" #include "system/logging.h" #include "system/passert.h" #include "services/common/evented_timer.h" #define FAIL_PAUSE_MS 1000 #define SCROLL_OUT_MS 250 #define BAR_HEIGHT 6 #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 UPDATE_INTERVAL 200 #define UPDATE_AMOUNT 2 #define FAILURE_PERCENT 15 #define INITIAL_PERCENT 0 //! App data typedef struct { //! UI ProgressWindow window; //! App fetch result AppFetchResult result; //! Data AppInstallEntry install_entry; AppFetchUIArgs next_app_args; EventServiceInfo fetch_event_info; EventServiceInfo connect_event_info; bool failed; } AppFetchUIData; static void prv_set_progress(AppFetchUIData *data, int16_t progress) { progress_window_set_progress(&data->window, progress); } // Launch the desired app static void prv_app_fetch_launch_app(AppFetchUIData *data) { // Let's launch the application we just fetched. PBL_LOG(LOG_LEVEL_DEBUG, "App Fetch: Putting launch event"); // if this was launched by the phone, it's probably a new install if ((data->next_app_args.common.reason == APP_LAUNCH_PHONE) && !battery_is_usb_connected()) { vibes_short_pulse(); } // Allocate and inialize the data that would have been sent to the app originally before the // fetch request. PebbleLaunchAppEventExtended *ext = kernel_malloc_check(sizeof(PebbleLaunchAppEventExtended)); *ext = (PebbleLaunchAppEventExtended) { .common = data->next_app_args.common, .wakeup = data->next_app_args.wakeup_info }; #if PLATFORM_TINTIN ext->common.transition = compositor_app_slide_transition_get(true /* slide to right */); #else ext->common.transition = compositor_dot_transition_app_fetch_get(); #endif if ((data->next_app_args.common.reason == APP_LAUNCH_WAKEUP) && (data->next_app_args.common.args != NULL)) { ext->common.args = &data->next_app_args.wakeup_info; } PebbleEvent launch_event = { .type = PEBBLE_APP_LAUNCH_EVENT, .launch_app = { .id = data->next_app_args.app_id, .data = ext } }; event_put(&launch_event); } /////////////////////////////// // Animation Related Functions /////////////////////////////// static void prv_remote_comm_session_event_handler(PebbleEvent *event, void *context) { AppFetchUIData *data = app_state_get_user_data(); if (event->bluetooth.comm_session_event.is_open && event->bluetooth.comm_session_event.is_system) { progress_window_pop(&data->window); } } static void prv_set_progress_failure(AppFetchUIData *data) { uint32_t icon; const char *message; switch (data->result) { case AppFetchResultNoBluetooth: icon = TIMELINE_RESOURCE_WATCH_DISCONNECTED; message = i18n_get("Not connected", data); // Subscribe to the BT remote app connect event data->connect_event_info = (EventServiceInfo) { .type = PEBBLE_COMM_SESSION_EVENT, .handler = prv_remote_comm_session_event_handler }; event_service_client_subscribe(&data->connect_event_info); break; case AppFetchResultNoData: icon = TIMELINE_RESOURCE_CHECK_INTERNET_CONNECTION; #if PBL_ROUND // TODO PBL-28730: Fix peek layer so it does its own line wrapping message = i18n_get("No internet\nconnection", data); #else message = i18n_get("No internet connection", data); #endif break; case AppFetchResultIncompatibleJSFailure: // TODO: PBL-39752 make this a more expressive error message with a call to action icon = TIMELINE_RESOURCE_GENERIC_WARNING; message = i18n_get("Incompatible JS", data); break; case AppFetchResultGeneralFailure: case AppFetchResultUUIDInvalid: case AppFetchResultPutBytesFailure: case AppFetchResultTimeoutError: case AppFetchResultPhoneBusy: default: icon = TIMELINE_RESOURCE_GENERIC_WARNING; message = i18n_get("Failed", data); break; } progress_window_set_result_failure(&data->window, icon, message, PROGRESS_WINDOW_DEFAULT_FAILURE_DELAY_MS); if (!battery_is_usb_connected()) { vibes_short_pulse(); } } static void prv_progress_window_finished(ProgressWindow *window, bool success, void *context) { AppFetchUIData *data = context; if (success) { prv_app_fetch_launch_app(data); } } //////////////////////////// // Internal Helper Functions //////////////////////////// //! Used to clean up the application's data before exiting static void prv_app_fetch_cleanup(AppFetchUIData *data) { PBL_LOG(LOG_LEVEL_DEBUG, "App Fetch: prv_app_fetch_cleanup"); event_service_client_unsubscribe(&data->fetch_event_info); event_service_client_unsubscribe(&data->connect_event_info); } //! Used when the app fetch process has failed static void prv_app_fetch_failure(AppFetchUIData *data, uint8_t error_code) { PBL_LOG(LOG_LEVEL_WARNING, "App Fetch: prv_app_fetch_failure: %d", error_code); if (error_code == AppFetchResultUserCancelled) { app_window_stack_pop(true); } data->result = error_code; if ((watchface_get_default_install_id() == data->install_entry.install_id) && app_install_entry_is_watchface(&data->install_entry)) { // We failed to fetch a watchface and it was our default. // Invalidate it and it will be reassigned to one that exists next time around. PBL_LOG(LOG_LEVEL_WARNING, "Default watchface fetch failed, setting INVALID as default"); watchface_set_default_install_id(INSTALL_ID_INVALID); } else if ((worker_manager_get_default_install_id() == data->install_entry.install_id) && app_install_entry_has_worker(&data->install_entry)) { // We failed to fetch a worker and it was our default. // Invalidate it and it will be reassigned to one that is launched next. PBL_LOG(LOG_LEVEL_WARNING, "Default worker fetch failed, setting INVALID as default"); worker_manager_set_default_install_id(INSTALL_ID_INVALID); } data->failed = true; prv_set_progress_failure(data); prv_app_fetch_cleanup(data); } //! App Fetch handler. Used for keeping track of progress and cleanup events static void prv_app_fetch_event_handler(PebbleEvent *event, void *context) { AppFetchUIData *data = app_state_get_user_data(); PebbleAppFetchEvent *af_event = (PebbleAppFetchEvent *) event; // We have starting the App Fetch Process if (af_event->type == AppFetchEventTypeStart) { PBL_LOG(LOG_LEVEL_DEBUG, "App Fetch: Got the start event"); // We have received a new progress event } else if (af_event->type == AppFetchEventTypeProgress) { progress_window_set_progress(&data->window, af_event->progress_percent); // We have finished the app fetch. Launching } else if (af_event->type == AppFetchEventTypeFinish) { progress_window_set_result_success(&data->window); prv_app_fetch_cleanup(data); // We received an error. Fail } else if (af_event->type == AppFetchEventTypeError) { prv_app_fetch_failure(data, af_event->error_code); } } // TODO: Use appropriate transitions to and from watchfaces or apps static void prv_click_handler(ClickRecognizerRef recognizer, Window *window) { AppFetchUIData *data = app_state_get_user_data(); if (data->failed) { app_window_stack_pop(true); } else { app_fetch_cancel(data->install_entry.install_id); } } static void config_provider(void *context) { window_single_click_subscribe(BUTTON_ID_BACK, (ClickHandler) prv_click_handler); window_single_click_subscribe(BUTTON_ID_UP, (ClickHandler) prv_click_handler); window_single_click_subscribe(BUTTON_ID_SELECT, (ClickHandler) prv_click_handler); window_single_click_subscribe(BUTTON_ID_BACK, (ClickHandler) prv_click_handler); } static void handle_init(void) { AppFetchUIData* data = app_zalloc_check(sizeof(AppFetchUIData)); app_state_set_user_data(data); // get app args, copy them to app memory, and free the kernel buffer AppFetchUIArgs *temp_fetch_args = (AppFetchUIArgs *)process_manager_get_current_process_args(); memcpy(&data->next_app_args, temp_fetch_args, sizeof(AppFetchUIArgs)); kernel_free(temp_fetch_args); // Create and set up window progress_window_init(&data->window); progress_window_set_callbacks(&data->window, (ProgressWindowCallbacks) { .finished = prv_progress_window_finished, }, data); window_set_click_config_provider((Window *)&data->window, config_provider); // retrieve data about the AppInstallId given if (!app_install_get_entry_for_install_id(data->next_app_args.app_id, &data->install_entry)) { PBL_LOG(LOG_LEVEL_ERROR, "App Fetch: Error getting entry for id: %"PRIu32"", data->next_app_args.app_id); return; } AppFetchError prev_error = app_fetch_get_previous_error(); if ((prev_error.id == data->next_app_args.app_id) && (prev_error.error != AppFetchResultSuccess)) { prv_app_fetch_failure(data, prev_error.error); prv_set_progress(data, FAILURE_PERCENT); } // subscribe to PutBytes events data->fetch_event_info = (EventServiceInfo) { .type = PEBBLE_APP_FETCH_EVENT, .handler = prv_app_fetch_event_handler }; event_service_client_subscribe(&data->fetch_event_info); app_progress_window_push(&data->window); } static void handle_deinit(void) { AppFetchUIData* data = app_state_get_user_data(); prv_app_fetch_cleanup(data); progress_window_deinit(&data->window); app_free(data); i18n_free_all(data); } static void s_main(void) { handle_init(); app_event_loop(); handle_deinit(); } const PebbleProcessMd *app_fetch_ui_get_app_info() { static const PebbleProcessMdSystem s_app_md = { .common = { .main_func = s_main, .visibility = ProcessVisibilityHidden, // UUID: 674271bc-f4fa-4536-97f3-8849a5ba75a4 .uuid = {0x67, 0x42, 0x71, 0xbc, 0xf4, 0xfa, 0x45, 0x36, 0x97, 0xf3, 0x88, 0x49, 0xa5, 0xba, 0x75, 0xa4}, }, .name = "App Fetch", }; return (const PebbleProcessMd*) &s_app_md; }