mirror of
https://github.com/google/pebble.git
synced 2025-07-06 23:00:29 -04:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
960
src/fw/process_management/app_manager.c
Normal file
960
src/fw/process_management/app_manager.c
Normal file
|
@ -0,0 +1,960 @@
|
|||
/*
|
||||
* 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_manager.h"
|
||||
#include "worker_manager.h"
|
||||
#include "process_loader.h"
|
||||
|
||||
// Pebble stuff
|
||||
#include "applib/app_launch_reason.h"
|
||||
#include "applib/app_message/app_message_internal.h"
|
||||
#include "applib/fonts/fonts.h"
|
||||
#include "applib/ui/dialogs/dialog.h"
|
||||
#include "applib/ui/dialogs/simple_dialog.h"
|
||||
#include "applib/ui/window_stack.h"
|
||||
#include "apps/system_app_ids.h"
|
||||
#include "console/prompt.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/util/segment.h"
|
||||
#include "kernel/util/task_init.h"
|
||||
#include "mcu/cache.h"
|
||||
#include "mcu/privilege.h"
|
||||
#include "os/mutex.h"
|
||||
#include "popups/health_tracking_ui.h"
|
||||
#include "popups/timeline/peek.h"
|
||||
#include "process_management/app_run_state.h"
|
||||
#include "process_management/pebble_process_md.h"
|
||||
#include "process_management/process_heap.h"
|
||||
#include "process_management/sdk_memory_limits.auto.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "resource/resource.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "resource/resource_mapped.h"
|
||||
#include "services/common/analytics/analytics.h"
|
||||
#include "services/common/compositor/compositor_transitions.h"
|
||||
#include "services/common/i18n/i18n.h"
|
||||
#include "services/common/light.h"
|
||||
#include "services/normal/app_cache.h"
|
||||
#include "services/normal/app_inbox_service.h"
|
||||
#include "services/normal/app_outbox_service.h"
|
||||
#include "shell/normal/app_idle_timeout.h"
|
||||
#include "shell/normal/watchface.h"
|
||||
#include "shell/shell.h"
|
||||
#include "shell/system_app_state_machine.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/size.h"
|
||||
|
||||
// FreeRTOS stuff
|
||||
#include "FreeRTOS.h"
|
||||
#include "freertos_application.h"
|
||||
#include "task.h"
|
||||
#include "queue.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define RETURN_CRASH_TIMEOUT_TICKS (60 * RTC_TICKS_HZ)
|
||||
|
||||
//! Behold! The file that manages applications!
|
||||
//!
|
||||
//! The code in this file applies to all apps, whether they're third party apps (stored in SPI flash) or first
|
||||
//! party apps stored inside our firmware.
|
||||
//!
|
||||
//! Apps are only started and stopped on the launcher task (aka kernel main).
|
||||
|
||||
extern char __APP_RAM__[];
|
||||
extern char __APP_RAM_end__[];
|
||||
extern char __stack_guard_size__[];
|
||||
|
||||
//! Used by the "pebble gdb" command to locate the loaded app in memory.
|
||||
void * volatile g_app_load_address;
|
||||
|
||||
static const int MAX_TO_APP_EVENTS = 32;
|
||||
static QueueHandle_t s_to_app_event_queue;
|
||||
static ProcessContext s_app_task_context;
|
||||
static ProcessAppRunLevel s_minimum_run_level;
|
||||
|
||||
typedef struct NextApp {
|
||||
LaunchConfigCommon common;
|
||||
const PebbleProcessMd *md;
|
||||
WakeupInfo wakeup_info;
|
||||
} NextApp;
|
||||
|
||||
typedef struct {
|
||||
AppInstallId install_id;
|
||||
RtcTicks crash_ticks;
|
||||
} AppCrashInfo;
|
||||
|
||||
static NextApp s_next_app;
|
||||
|
||||
static void prv_handle_app_start_analytics(const PebbleProcessMd *app_md,
|
||||
const AppLaunchReason launch_reason);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
void app_manager_init(void) {
|
||||
s_to_app_event_queue = xQueueCreate(MAX_TO_APP_EVENTS, sizeof(PebbleEvent));
|
||||
|
||||
s_app_task_context = (ProcessContext) { 0 };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
bool app_manager_is_initialized(void) {
|
||||
return s_to_app_event_queue != NULL;
|
||||
}
|
||||
|
||||
static bool s_first_app_launched = false;
|
||||
bool app_manager_is_first_app_launched(void) {
|
||||
return s_first_app_launched;
|
||||
}
|
||||
|
||||
WakeupInfo app_manager_get_app_wakeup_state(void) {
|
||||
return s_next_app.wakeup_info;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
//! This is the wrapper function for all apps here. It's not allowed to return as it's
|
||||
//! the top frame on the stack created for the application.
|
||||
static void prv_app_task_main(void *entry_point) {
|
||||
app_state_init();
|
||||
task_init();
|
||||
|
||||
// about to start the app in earnest. No longer safe to kill.
|
||||
s_app_task_context.safe_to_kill = false;
|
||||
|
||||
// Enter unprivileged mode!
|
||||
const bool is_unprivileged = s_app_task_context.app_md->is_unprivileged;
|
||||
|
||||
// There are currently no Rocky.js APIs that need to be called while in privileged mode, so run
|
||||
// in unprivileged mode for the built-in Rocky.js apps (Tictoc) as well:
|
||||
const bool is_rocky_app = s_app_task_context.app_md->is_rocky_app;
|
||||
|
||||
if (is_unprivileged || is_rocky_app) {
|
||||
mcu_state_set_thread_privilege(false);
|
||||
}
|
||||
|
||||
const PebbleMain main_func = entry_point;
|
||||
main_func();
|
||||
|
||||
// Clean up after the app. Remember to put only non-critical cleanup here,
|
||||
// as the app may crash or otherwise misbehave. If something really needs to
|
||||
// be cleaned up, make it so the kernel can do it on the apps behalf and put
|
||||
// the call at the bottom of prv_app_cleanup.
|
||||
app_state_deinit();
|
||||
#ifndef RECOVERY_FW
|
||||
app_message_close();
|
||||
#endif
|
||||
|
||||
sys_exit();
|
||||
}
|
||||
|
||||
//! Heap locking function for our app heap. Our process heaps don't actually
|
||||
//! have to be locked because they're the sole property of the process and no
|
||||
//! other tasks should be touching it. All this function does is verify that
|
||||
//! this condition is met before continuing without locking.
|
||||
static void prv_heap_lock(void* unused) {
|
||||
PBL_ASSERT_TASK(PebbleTask_App);
|
||||
}
|
||||
|
||||
void prv_dump_start_app_info(const PebbleProcessMd *app_md) {
|
||||
char *app_type = "";
|
||||
switch (process_metadata_get_app_sdk_type(app_md)) {
|
||||
case ProcessAppSDKType_System:
|
||||
app_type = "system";
|
||||
break;
|
||||
case ProcessAppSDKType_Legacy2x:
|
||||
app_type = "legacy2";
|
||||
break;
|
||||
case ProcessAppSDKType_Legacy3x:
|
||||
app_type = "legacy3";
|
||||
break;
|
||||
case ProcessAppSDKType_4x:
|
||||
app_type = "4.x";
|
||||
break;
|
||||
}
|
||||
|
||||
char *const sdk_platform = platform_type_get_name(process_metadata_get_app_sdk_platform(app_md));
|
||||
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Starting %s app <%s>", app_type, process_metadata_get_name(app_md));
|
||||
// new logging only allows for 2 %s per format string...
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Starting app with sdk platform %s", sdk_platform);
|
||||
}
|
||||
|
||||
#define APP_STACK_ROCKY_SIZE (8 * 1024)
|
||||
#define APP_STACK_NORMAL_SIZE (2 * 1024)
|
||||
|
||||
static size_t prv_get_app_segment_size(const PebbleProcessMd *app_md) {
|
||||
switch (process_metadata_get_app_sdk_type(app_md)) {
|
||||
case ProcessAppSDKType_Legacy2x:
|
||||
return APP_RAM_2X_SIZE;
|
||||
case ProcessAppSDKType_Legacy3x:
|
||||
return APP_RAM_3X_SIZE;
|
||||
case ProcessAppSDKType_4x:
|
||||
#if CAPABILITY_HAS_JAVASCRIPT
|
||||
if (app_md->is_rocky_app) {
|
||||
// on Spalding, we didn't have enough applib padding to guarantee both,
|
||||
// 4.x native app heap + JerryScript statis + increased stack for Rocky.
|
||||
// For now, we just decrease the amount of available heap as we don't use it.
|
||||
// In the future, we will move the JS stack to the heap PBL-35783,
|
||||
// make byte code swappable PBL-37937,and remove JerryScript's static PBL-40400.
|
||||
// All of the above will work to our advantage so it's safe to make this simple
|
||||
// change now.
|
||||
return APP_RAM_4X_SIZE - (APP_STACK_ROCKY_SIZE - APP_STACK_NORMAL_SIZE);
|
||||
}
|
||||
#endif
|
||||
return APP_RAM_4X_SIZE;
|
||||
case ProcessAppSDKType_System:
|
||||
return APP_RAM_SYSTEM_SIZE;
|
||||
default:
|
||||
WTF;
|
||||
}
|
||||
}
|
||||
|
||||
static size_t prv_get_app_stack_size(const PebbleProcessMd *app_md) {
|
||||
#if CAPABILITY_HAS_JAVASCRIPT
|
||||
if (app_md->is_rocky_app) {
|
||||
return APP_STACK_ROCKY_SIZE;
|
||||
}
|
||||
#endif
|
||||
return APP_STACK_NORMAL_SIZE;
|
||||
}
|
||||
|
||||
T_STATIC MemorySegment prv_get_app_ram_segment(void) {
|
||||
return (MemorySegment) { __APP_RAM__, __APP_RAM_end__ };
|
||||
}
|
||||
|
||||
T_STATIC size_t prv_get_stack_guard_size(void) {
|
||||
return (uintptr_t)__stack_guard_size__;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
//! @return True on success, False if:
|
||||
//! - We fail to start the app. No app is running and the caller is responsible for starting
|
||||
//! a different app.
|
||||
//!
|
||||
//! @note Side effects: trips assertions if:
|
||||
//! - The app manager was not init,
|
||||
//! - The app's task handle or event queue aren't null
|
||||
//! - The app's metadata is null
|
||||
static bool prv_app_start(const PebbleProcessMd *app_md, const void *args,
|
||||
const AppLaunchReason launch_reason) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelMain);
|
||||
PBL_ASSERTN(app_md);
|
||||
|
||||
prv_dump_start_app_info(app_md);
|
||||
|
||||
process_manager_init_context(&s_app_task_context, app_md, args);
|
||||
|
||||
// Set up the app's memory and load the app into it.
|
||||
size_t app_segment_size = prv_get_app_segment_size(app_md);
|
||||
// The stack guard is counted as part of the app segment size...
|
||||
const size_t stack_guard_size = prv_get_stack_guard_size();
|
||||
// ...and is carved out of the stack.
|
||||
const size_t stack_size = prv_get_app_stack_size(app_md) - stack_guard_size;
|
||||
|
||||
MemorySegment app_ram = prv_get_app_ram_segment();
|
||||
|
||||
#if !UNITTEST
|
||||
if (app_md->is_rocky_app) {
|
||||
/* PBL-40376: Temp hack: put .rocky_bss at end of APP_RAM:
|
||||
Interim solution until all statics are removed from applib & jerry.
|
||||
These statics are only used for rocky apps, so it's OK that this overlaps/overlays with the
|
||||
app heap for non-rocky apps.
|
||||
*/
|
||||
extern char __ROCKY_BSS_size__[];
|
||||
extern char __ROCKY_BSS__[];
|
||||
memset(__ROCKY_BSS__, 0, (size_t)__ROCKY_BSS_size__);
|
||||
|
||||
// ROCKY_BSS is inside APP_RAM to make the syscall buffer checks pass.
|
||||
// However, we want to avoid overlapping with any splits we're about to make:
|
||||
app_ram.end = __ROCKY_BSS__;
|
||||
|
||||
// Reduce the size available for the code + app heap, on Spalding the "padding" we had left
|
||||
// isn't enough to fit Rocky + Jerry's .bss:
|
||||
app_segment_size -= 1400;
|
||||
}
|
||||
#endif
|
||||
|
||||
memset((char *)app_ram.start + stack_guard_size, 0,
|
||||
memory_segment_get_size(&app_ram) - stack_guard_size);
|
||||
|
||||
MemorySegment app_segment;
|
||||
PBL_ASSERTN(memory_segment_split(&app_ram, &app_segment, app_segment_size));
|
||||
PBL_ASSERTN(memory_segment_split(&app_segment, NULL, stack_guard_size));
|
||||
// No (accessible) memory segments can be placed between the top of APP_RAM
|
||||
// and the end of stack. Stacks always grow towards lower memory addresses, so
|
||||
// we want a stack overflow to touch the stack guard region before it begins
|
||||
// to clobber actual data. And syscalls assume that the stack is always at the
|
||||
// top of APP_RAM; violating this assumption will result in syscalls sometimes
|
||||
// failing when the app hasn't done anything wrong.
|
||||
portSTACK_TYPE *stack = memory_segment_split(&app_segment, NULL, stack_size);
|
||||
PBL_ASSERTN(stack);
|
||||
s_app_task_context.load_start = app_segment.start;
|
||||
g_app_load_address = app_segment.start;
|
||||
void *entry_point = process_loader_load(app_md, PebbleTask_App, &app_segment);
|
||||
s_app_task_context.load_end = app_segment.start;
|
||||
if (!entry_point) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Tried to launch an invalid app in bank %u!",
|
||||
process_metadata_get_code_bank_num(app_md));
|
||||
return false;
|
||||
}
|
||||
|
||||
const ResAppNum res_bank_num = process_metadata_get_res_bank_num(app_md);
|
||||
if (res_bank_num != SYSTEM_APP) {
|
||||
const ResourceVersion res_version = process_metadata_get_res_version(app_md);
|
||||
// for RockyJS apps, we initialize without checking the for a match between
|
||||
// binary's copy of the resource CRC and the actual CRC as it could be outdated
|
||||
const ResourceVersion *const res_version_ptr = app_md->is_rocky_app ? NULL : &res_version;
|
||||
if (!resource_init_app(res_bank_num, res_version_ptr)) {
|
||||
// The resources are busted! Abort starting this app.
|
||||
APP_LOG(APP_LOG_LEVEL_ERROR,
|
||||
"Checksum for resources differs or insufficient meta data for JavaScript app.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Synchronously handle process start since its new state is needed for app state initialization
|
||||
timeline_peek_handle_process_start();
|
||||
|
||||
const ProcessAppSDKType sdk_type = process_metadata_get_app_sdk_type(app_md);
|
||||
|
||||
// The rest of app_ram is available for app_state to use as it sees fit.
|
||||
if (!app_state_configure(&app_ram, sdk_type, timeline_peek_get_obstruction_origin_y())) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "App state configuration failed");
|
||||
return false;
|
||||
}
|
||||
// The remaining space in app_segment is assigned to the app's heap.
|
||||
// app_state needs to be configured before initializing the app heap
|
||||
// as the AppState struct holds the app heap's Heap object.
|
||||
|
||||
// Don't fuzz 3rd party app heaps because likely many of them rely on accessing free'd memory
|
||||
bool enable_heap_fuzzing = (sdk_type == ProcessAppSDKType_System);
|
||||
Heap *app_heap = app_state_get_heap();
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "App heap init %p %p",
|
||||
app_segment.start, app_segment.end);
|
||||
heap_init(app_heap, app_segment.start, app_segment.end, enable_heap_fuzzing);
|
||||
heap_set_lock_impl(app_heap, (HeapLockImpl) {
|
||||
.lock_function = prv_heap_lock,
|
||||
});
|
||||
process_heap_set_exception_handlers(app_heap, app_md);
|
||||
|
||||
// We're now going to start the app. We can't abort the app now without calling prv_app_cleanup.
|
||||
|
||||
// If it's a watchface and we were launched by the phone or the user, make it the new default.
|
||||
if ((s_app_task_context.install_id != INSTALL_ID_INVALID) &&
|
||||
((launch_reason == APP_LAUNCH_PHONE) || (launch_reason == APP_LAUNCH_USER))) {
|
||||
AppInstallEntry entry;
|
||||
if (!app_install_get_entry_for_install_id(s_app_task_context.install_id, &entry)) {
|
||||
// cant retrieve app install entry for id
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Failed to get entry for id %"PRId32, s_app_task_context.install_id);
|
||||
return false;
|
||||
}
|
||||
if (app_install_entry_is_watchface(&entry) && !app_install_entry_is_hidden(&entry)) {
|
||||
watchface_set_default_install_id(entry.install_id);
|
||||
}
|
||||
}
|
||||
|
||||
app_manager_set_minimum_run_level(process_metadata_get_run_level(app_md));
|
||||
|
||||
// Use the static app event queue:
|
||||
s_app_task_context.to_process_event_queue = s_to_app_event_queue;
|
||||
|
||||
// Init services required for this process before it starts to execute
|
||||
process_manager_process_setup(PebbleTask_App);
|
||||
|
||||
char task_name[configMAX_TASK_NAME_LEN];
|
||||
snprintf(task_name, sizeof(task_name), "App <%s>", process_metadata_get_name(s_app_task_context.app_md));
|
||||
|
||||
TaskParameters_t task_params = {
|
||||
.pvTaskCode = prv_app_task_main,
|
||||
.pcName = task_name,
|
||||
.usStackDepth = stack_size / sizeof(portSTACK_TYPE),
|
||||
.pvParameters = entry_point,
|
||||
.uxPriority = APP_TASK_PRIORITY | portPRIVILEGE_BIT,
|
||||
.puxStackBuffer = stack,
|
||||
};
|
||||
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Starting %s", task_name);
|
||||
|
||||
// Store slot of launched app for reboot support (flash apps only)
|
||||
reboot_set_slot_of_last_launched_app(
|
||||
(app_md->process_storage == ProcessStorageFlash) ?
|
||||
process_metadata_get_code_bank_num(app_md) : SYSTEM_APP_BANK_ID);
|
||||
|
||||
pebble_task_create(PebbleTask_App, &task_params, &s_app_task_context.task_handle);
|
||||
|
||||
// Always notify the phone that the application is running
|
||||
app_run_state_send_update(&app_md->uuid, RUNNING);
|
||||
|
||||
system_app_state_machine_register_app_launch(s_app_task_context.install_id);
|
||||
|
||||
prv_handle_app_start_analytics(app_md, launch_reason);
|
||||
|
||||
#if CAPABILITY_HAS_HEALTH_TRACKING && !defined(RECOVERY_FW)
|
||||
health_tracking_ui_register_app_launch(s_app_task_context.install_id);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
//! Kills the app, giving it no chance to clean things up or exit gracefully. The app must
|
||||
//! already be in a state where it's safe to exit.
|
||||
//! Note that the app may not have ever been successfully started when this is called, so check
|
||||
//! your null pointers!
|
||||
static void prv_app_cleanup(void) {
|
||||
// Back button may have been held down when this app quits.
|
||||
launcher_cancel_force_quit();
|
||||
|
||||
// Always notify the phone that the application is not running
|
||||
app_run_state_send_update(&s_app_task_context.app_md->uuid, NOT_RUNNING);
|
||||
|
||||
// Perform generic process cleanup. Note that s_app_task_context will be cleaned up and zero'd
|
||||
// by this.
|
||||
process_manager_process_cleanup(PebbleTask_App);
|
||||
|
||||
// Perform app specific cleanup
|
||||
app_idle_timeout_stop();
|
||||
#ifndef RECOVERY_FW
|
||||
app_inbox_service_unregister_all();
|
||||
app_outbox_service_cleanup_all_pending_messages();
|
||||
#endif
|
||||
light_reset_user_controlled();
|
||||
sys_vibe_history_stop_collecting();
|
||||
#if !defined(PLATFORM_TINTIN)
|
||||
ble_app_cleanup();
|
||||
#endif
|
||||
#if CAPABILITY_HAS_MAPPABLE_FLASH
|
||||
resource_mapped_release_all(PebbleTask_App);
|
||||
#endif
|
||||
|
||||
app_comm_set_sniff_interval(SNIFF_INTERVAL_NORMAL);
|
||||
|
||||
app_manager_set_minimum_run_level(ProcessAppRunLevelNormal);
|
||||
app_install_cleanup_registered_app_callbacks();
|
||||
app_install_notify_app_closed();
|
||||
|
||||
timeline_peek_handle_process_kill();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
//! On watchface crashes, we want to signal to the user that the watchface has crashed so that
|
||||
//! they understand why are being jettisoned into the launcher.
|
||||
static void prv_app_show_crash_ui(AppInstallId install_id) {
|
||||
AppInstallEntry entry;
|
||||
|
||||
if (!app_install_get_entry_for_install_id(install_id, &entry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!app_install_entry_is_watchface(&entry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if !defined(RECOVERY_FW)
|
||||
static AppCrashInfo crash_info = { 0 };
|
||||
// If the same watchface crashes twice in one minute, then we show a dialog informing
|
||||
// the user that the watchface has crashed. Any button press will dismiss
|
||||
// the dialog and show us the default system watch face.
|
||||
PBL_ASSERTN(install_id != INSTALL_ID_INVALID);
|
||||
if (crash_info.install_id != install_id ||
|
||||
(crash_info.crash_ticks + RETURN_CRASH_TIMEOUT_TICKS) < rtc_get_ticks()) {
|
||||
crash_info = (AppCrashInfo) {
|
||||
.install_id = install_id,
|
||||
.crash_ticks = rtc_get_ticks()
|
||||
};
|
||||
// Re-launch immediately
|
||||
watchface_launch_default(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
SimpleDialog *crash_dialog = simple_dialog_create("Watchface crashed");
|
||||
Dialog *dialog = simple_dialog_get_dialog(crash_dialog);
|
||||
const char *text_fmt = i18n_get("%.*s is not responding", crash_dialog);
|
||||
unsigned int name_len = 15;
|
||||
char text[DIALOG_MAX_MESSAGE_LEN];
|
||||
sniprintf(text, DIALOG_MAX_MESSAGE_LEN, text_fmt, name_len, entry.name);
|
||||
|
||||
dialog_set_text(dialog, text);
|
||||
dialog_set_icon(dialog, RESOURCE_ID_GENERIC_WARNING_LARGE);
|
||||
dialog_set_timeout(dialog, DIALOG_TIMEOUT_INFINITE /* no timeout */);
|
||||
|
||||
// Any sort of application crash or window crash is a critical message as it
|
||||
// impacts the UX experience, so we want to push it to the forefront of the
|
||||
// window stack.
|
||||
WindowStack *window_stack = modal_manager_get_window_stack(ModalPriorityAlert);
|
||||
simple_dialog_push(crash_dialog, window_stack);
|
||||
|
||||
#if PBL_ROUND
|
||||
// For circular display, reduce app name length until message fits on the screen
|
||||
// This has to occur after the dialog window load has been called to provide
|
||||
// initial layout, text_layer flow and text_layer positions
|
||||
TextLayer *text_layer = &dialog->text_layer;
|
||||
const unsigned int min_text_len = 3;
|
||||
const int max_text_height = 2 * fonts_get_font_height(text_layer->font) + 8;
|
||||
GContext *ctx = graphics_context_get_current_context();
|
||||
int32_t text_height = text_layer_get_content_size(ctx, text_layer).h;
|
||||
|
||||
// Until the text_height fits max_text_height or the app name is min_text_len
|
||||
while (text_height > max_text_height && name_len > min_text_len) {
|
||||
name_len--;
|
||||
sniprintf(text, DIALOG_MAX_MESSAGE_LEN, text_fmt, name_len, entry.name);
|
||||
dialog_set_text(dialog, text);
|
||||
text_height = text_layer_get_content_size(ctx, text_layer).h;
|
||||
}
|
||||
#endif
|
||||
|
||||
i18n_free_all(crash_dialog);
|
||||
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Watchface crashed, launching default.");
|
||||
|
||||
crash_info = (AppCrashInfo) { 0 };
|
||||
|
||||
watchface_set_default_install_id(INSTALL_ID_INVALID);
|
||||
watchface_launch_default(NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
//! Switch to the app stored in the s_next_app global. The gracefully flag tells us whether to attempt a graceful
|
||||
//! exit or not.
|
||||
//!
|
||||
//! For a graceful exit, if the app has not alreeady finished it's de-init, we post a de_init event to the app, set
|
||||
//! a 3 second timer, and return immediately to the caller. If/when the app finally finishes deinit, it will post a
|
||||
//! PEBBLE_PROCESS_KILL_EVENT (graceful=true), which results in this method being again with graceful=true. We will then
|
||||
//! see that the de_init already finished in that second invocation.
|
||||
//!
|
||||
//! If the app has finished its de-init, or graceful is false, we proceed to kill the app task and launch the next
|
||||
//! app as stored in the s_next_app global.
|
||||
//!
|
||||
//! Returns true if new app was just switched in.
|
||||
static bool prv_app_switch(bool gracefully) {
|
||||
ProcessContext *app_task_ctx = &s_app_task_context;
|
||||
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Switching from '%s' to '%s', graceful=%d...",
|
||||
process_metadata_get_name(app_task_ctx->app_md),
|
||||
process_metadata_get_name(s_next_app.md),
|
||||
(int)gracefully);
|
||||
|
||||
// Shouldn't be called from app. Use app_manager_put_kill_app_event() instead.
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelMain);
|
||||
|
||||
// We have to call this here, in addition to calling it in prv_app_cleanup(),
|
||||
// because the timer could otherwise be triggered while waiting for the task
|
||||
// to exit, causing the app we land on to be killed when it shouldn't be.
|
||||
launcher_cancel_force_quit();
|
||||
|
||||
// Make sure the process is safe to kill. If this method returns false, it will have set a timer to post
|
||||
// another KILL event in a few seconds, thus giving the process a chance to clean up.
|
||||
if (!process_manager_make_process_safe_to_kill(PebbleTask_App, gracefully)) {
|
||||
// Maybe next time...
|
||||
return false;
|
||||
}
|
||||
|
||||
AppInstallId old_install_id = s_app_task_context.install_id;
|
||||
|
||||
// Kill the current app
|
||||
prv_app_cleanup();
|
||||
|
||||
// If we had to ungracefully kill the current app, switch to the launcher app
|
||||
if (!gracefully) {
|
||||
app_install_release_md(s_next_app.md);
|
||||
s_next_app = (NextApp) {
|
||||
.md = system_app_state_machine_get_default_app(),
|
||||
};
|
||||
} else {
|
||||
// Get the next app to launch
|
||||
if (!s_next_app.md) {
|
||||
// There is no next app to launch? We're starting up, let's launch the startup app.
|
||||
app_install_release_md(s_next_app.md);
|
||||
s_next_app = (NextApp) {
|
||||
.md = system_app_state_machine_system_start(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Launch the new app
|
||||
if (!prv_app_start(s_next_app.md, s_next_app.common.args, s_next_app.common.reason)) {
|
||||
if (s_next_app.md->process_storage != ProcessStorageFlash) {
|
||||
PBL_CROAK("Failed to start system app <%s>!", process_metadata_get_name(s_next_app.md));
|
||||
}
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Failed to start app <%s>! Restarting launcher",
|
||||
process_metadata_get_name(s_next_app.md));
|
||||
|
||||
prv_app_start(system_app_state_machine_system_start(), NULL, APP_LAUNCH_SYSTEM);
|
||||
}
|
||||
|
||||
compositor_transition(s_next_app.common.transition);
|
||||
|
||||
// Check if we've exited gracefully. Otherwise, display the crash dialog if appropriate.
|
||||
if (!gracefully) {
|
||||
prv_app_show_crash_ui(old_install_id);
|
||||
}
|
||||
|
||||
// Clear for next time.
|
||||
s_next_app = (NextApp) {};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
void app_manager_start_first_app(void) {
|
||||
const PebbleProcessMd* app_md = system_app_state_machine_system_start();
|
||||
PBL_ASSERTN(prv_app_start(app_md, 0, APP_LAUNCH_SYSTEM));
|
||||
s_first_app_launched = true;
|
||||
compositor_transition(NULL);
|
||||
}
|
||||
|
||||
static const CompositorTransition *prv_get_transition(const LaunchConfigCommon *config,
|
||||
AppInstallId new_app_id) {
|
||||
return config->transition ?: shell_get_open_compositor_animation(s_app_task_context.install_id,
|
||||
new_app_id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
void app_manager_put_launch_app_event(const AppLaunchEventConfig *config) {
|
||||
PBL_ASSERTN(config->id != INSTALL_ID_INVALID);
|
||||
|
||||
PebbleLaunchAppEventExtended *data = kernel_malloc_check(sizeof(PebbleLaunchAppEventExtended));
|
||||
*data = (PebbleLaunchAppEventExtended) {
|
||||
.common = config->common
|
||||
};
|
||||
data->common.transition = prv_get_transition(&config->common, config->id);
|
||||
|
||||
PebbleEvent e = {
|
||||
.type = PEBBLE_APP_LAUNCH_EVENT,
|
||||
.launch_app = {
|
||||
.id = config->id,
|
||||
.data = data
|
||||
},
|
||||
};
|
||||
|
||||
event_put(&e);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
bool app_manager_launch_new_app(const AppLaunchConfig *config) {
|
||||
// Note that config has a dynamically allocated member that needs to be free'd with
|
||||
// app_install_release_md if we don't actually proceed with launching the app.
|
||||
|
||||
const PebbleProcessMd *app_md = config->md;
|
||||
const AppInstallId new_app_id = app_install_get_id_for_uuid(&app_md->uuid);
|
||||
|
||||
if (!config->restart && uuid_equal(&(app_md->uuid), &(s_app_task_context.app_md->uuid))) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Ignoring launch for app <%s>, app is already running",
|
||||
process_metadata_get_name(app_md));
|
||||
|
||||
app_install_release_md(app_md);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (process_metadata_get_run_level(app_md) < s_minimum_run_level) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING,
|
||||
"Ignoring launch for app <%s>, minimum run level %d, app run level %d",
|
||||
process_metadata_get_name(app_md), s_minimum_run_level,
|
||||
process_metadata_get_run_level(app_md));
|
||||
|
||||
app_install_release_md(app_md);
|
||||
return false;
|
||||
}
|
||||
|
||||
s_next_app = (NextApp) {
|
||||
.md = app_md,
|
||||
.common = config->common,
|
||||
};
|
||||
s_next_app.common.transition = prv_get_transition(&config->common, new_app_id);
|
||||
|
||||
if ((config->common.reason == APP_LAUNCH_WAKEUP) && (config->common.args != NULL)) {
|
||||
WakeupInfo *wakeup_info = (WakeupInfo *)config->common.args;
|
||||
s_next_app.wakeup_info = *(WakeupInfo *)wakeup_info;
|
||||
|
||||
// Stop pointing at the old storage location for wakeup_info so we don't keep the dangling
|
||||
// pointer around.
|
||||
s_next_app.common.args = NULL;
|
||||
}
|
||||
|
||||
return prv_app_switch(!config->forcefully);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
void app_manager_handle_app_fetch_request_event(const PebbleAppFetchRequestEvent *const evt) {
|
||||
PBL_ASSERTN(evt);
|
||||
if (!evt->with_ui) {
|
||||
return;
|
||||
}
|
||||
const AppFetchUIArgs *const fetch_args = evt->fetch_args;
|
||||
app_manager_launch_new_app(&(AppLaunchConfig) {
|
||||
.md = app_fetch_ui_get_app_info(),
|
||||
.common.args = fetch_args,
|
||||
.common.transition = fetch_args->common.transition,
|
||||
.forcefully = fetch_args->forcefully,
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
static AppInstallId prv_get_app_exit_reason_destination_install_id_override(void) {
|
||||
switch (s_app_task_context.exit_reason) {
|
||||
case APP_EXIT_NOT_SPECIFIED:
|
||||
return INSTALL_ID_INVALID;
|
||||
case APP_EXIT_ACTION_PERFORMED_SUCCESSFULLY:
|
||||
PBL_LOG(LOG_LEVEL_INFO,
|
||||
"Next app overridden with watchface because action was performed successfully");
|
||||
return watchface_get_default_install_id();
|
||||
// Handling this case specifically instead of providing a default case ensures that the addition
|
||||
// of future exit reason values will cause compilation to fail until the new case is handled
|
||||
case NUM_EXIT_REASONS:
|
||||
break;
|
||||
}
|
||||
WTF;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
void app_manager_close_current_app(bool gracefully) {
|
||||
// This method can be called as a result of receiving a PEBBLE_PROCESS_KILL_EVENT notification
|
||||
// from an app, telling us that it just finished it's deinit. Don't replace s_next_app.md if
|
||||
// perhaps it was already set by someone who called app_manager_launch_new_app or
|
||||
// app_manager_launch_new_app_with_args and asked the current app to exit.
|
||||
const AppInstallId current_app_id = s_app_task_context.install_id;
|
||||
AppInstallId destination_app_id = INSTALL_ID_INVALID;
|
||||
|
||||
#if !RECOVERY_FW
|
||||
destination_app_id = prv_get_app_exit_reason_destination_install_id_override();
|
||||
#endif
|
||||
|
||||
if (destination_app_id == INSTALL_ID_INVALID) {
|
||||
// If we get here, the app exit reason didn't override the destination app ID
|
||||
if (!s_next_app.md) {
|
||||
destination_app_id = system_app_state_machine_get_last_registered_app();
|
||||
} else {
|
||||
// If we get here, s_next_app is already setup and so we can call prv_app_switch() directly
|
||||
// and return
|
||||
prv_app_switch(gracefully);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
app_manager_set_minimum_run_level(ProcessAppRunLevelNormal);
|
||||
process_manager_launch_process(&(ProcessLaunchConfig) {
|
||||
.id = destination_app_id,
|
||||
.common.transition = shell_get_close_compositor_animation(current_app_id, destination_app_id),
|
||||
.forcefully = !gracefully,
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
void app_manager_set_minimum_run_level(ProcessAppRunLevel run_level) {
|
||||
s_minimum_run_level = run_level;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
void app_manager_force_quit_to_launcher(void) {
|
||||
const PebbleProcessMd *default_process = system_app_state_machine_get_default_app();
|
||||
const AppInstallId current_app_id = s_app_task_context.install_id;
|
||||
const AppInstallId new_app_id = app_install_get_id_for_uuid(&default_process->uuid);
|
||||
s_next_app = (NextApp) {
|
||||
.md = default_process,
|
||||
};
|
||||
s_next_app.common.transition = shell_get_close_compositor_animation(current_app_id, new_app_id);
|
||||
|
||||
prv_app_switch(true /*gracefully*/);
|
||||
}
|
||||
|
||||
const PebbleProcessMd* app_manager_get_current_app_md(void) {
|
||||
return s_app_task_context.app_md;
|
||||
}
|
||||
|
||||
AppInstallId app_manager_get_current_app_id(void) {
|
||||
return s_app_task_context.install_id;
|
||||
}
|
||||
|
||||
ProcessContext* app_manager_get_task_context(void) {
|
||||
return &s_app_task_context;
|
||||
}
|
||||
|
||||
bool app_manager_is_watchface_running(void) {
|
||||
return (app_manager_get_current_app_md()->process_type == ProcessTypeWatchface);
|
||||
}
|
||||
|
||||
ResAppNum app_manager_get_current_resource_num(void) {
|
||||
return process_metadata_get_res_bank_num(s_app_task_context.app_md);
|
||||
}
|
||||
|
||||
AppLaunchReason app_manager_get_launch_reason(void) {
|
||||
return s_next_app.common.reason;
|
||||
}
|
||||
|
||||
ButtonId app_manager_get_launch_button(void) {
|
||||
return s_next_app.common.button;
|
||||
}
|
||||
|
||||
void app_manager_get_framebuffer_size(GSize *size) {
|
||||
if (size == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!s_app_task_context.app_md) {
|
||||
// No app has been started yet, so just use the default system size
|
||||
*size = GSize(DISP_COLS, DISP_ROWS);
|
||||
return;
|
||||
}
|
||||
|
||||
// Platform matches current platform
|
||||
const PlatformType sdk_platform =
|
||||
process_metadata_get_app_sdk_platform(s_app_task_context.app_md);
|
||||
|
||||
if (sdk_platform == PBL_PLATFORM_TYPE_CURRENT) {
|
||||
*size = GSize(DISP_COLS, DISP_ROWS);
|
||||
return;
|
||||
}
|
||||
|
||||
// We cannot use the SDK type for this compatibility check but there's
|
||||
// also no easy way to get the resolutions per platform.
|
||||
// so we re-use the suboptimal defines from each display_<model>.h
|
||||
switch (sdk_platform) {
|
||||
case PlatformTypeAplite:
|
||||
*size = GSize(LEGACY_2X_DISP_COLS, LEGACY_2X_DISP_ROWS);
|
||||
return;
|
||||
case PlatformTypeBasalt:
|
||||
case PlatformTypeChalk:
|
||||
// yes, this is misleading, e.g. on Spalding, these defines are always 180x180
|
||||
// oh dear...
|
||||
*size = GSize(LEGACY_3X_DISP_COLS, LEGACY_3X_DISP_ROWS);
|
||||
return;
|
||||
case PlatformTypeDiorite:
|
||||
case PlatformTypeEmery:
|
||||
*size = GSize(DISP_COLS, DISP_ROWS);
|
||||
return;
|
||||
}
|
||||
WTF;
|
||||
}
|
||||
|
||||
bool app_manager_is_app_supported(const PebbleProcessMd *md) {
|
||||
// Get the app ram size depending on the SDK type.
|
||||
// Unsupported SDK types will have a size of 0.
|
||||
return prv_get_app_segment_size(md) > 0;
|
||||
}
|
||||
|
||||
|
||||
// Commands
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
void command_get_active_app_metadata(void) {
|
||||
char buffer[32];
|
||||
|
||||
const PebbleProcessMd* app_metadata = app_manager_get_current_app_md();
|
||||
if (app_metadata != NULL) {
|
||||
prompt_send_response_fmt(buffer, sizeof(buffer), "app name: %s",
|
||||
process_metadata_get_name(app_metadata));
|
||||
prompt_send_response_fmt(buffer, sizeof(buffer), "is watchface: %d",
|
||||
(app_metadata->process_type == ProcessTypeWatchface));
|
||||
prompt_send_response_fmt(buffer, sizeof(buffer), "visibility: %u", app_metadata->visibility);
|
||||
prompt_send_response_fmt(buffer, sizeof(buffer), "bank: %d",
|
||||
(uint8_t) process_metadata_get_res_bank_num(app_metadata));
|
||||
} else {
|
||||
prompt_send_response("metadata lookup failed: no app running");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Analytics
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
static void prv_handle_app_start_analytics(const PebbleProcessMd *app_md,
|
||||
const AppLaunchReason launch_reason) {
|
||||
analytics_event_app_launch(&app_md->uuid);
|
||||
analytics_inc(ANALYTICS_APP_METRIC_LAUNCH_COUNT, AnalyticsClient_App);
|
||||
analytics_stopwatch_start(ANALYTICS_APP_METRIC_FRONT_MOST_TIME, AnalyticsClient_App);
|
||||
|
||||
Version app_sdk_version = process_metadata_get_sdk_version(app_md);
|
||||
analytics_set(ANALYTICS_APP_METRIC_SDK_MAJOR_VERSION, app_sdk_version.major, AnalyticsClient_App);
|
||||
analytics_set(ANALYTICS_APP_METRIC_SDK_MINOR_VERSION, app_sdk_version.minor, AnalyticsClient_App);
|
||||
|
||||
Version app_version = process_metadata_get_process_version(app_md);
|
||||
analytics_set(ANALYTICS_APP_METRIC_APP_MAJOR_VERSION, app_version.major, AnalyticsClient_App);
|
||||
analytics_set(ANALYTICS_APP_METRIC_APP_MINOR_VERSION, app_version.minor, AnalyticsClient_App);
|
||||
|
||||
ResourceVersion resource_version = process_metadata_get_res_version(app_md);
|
||||
analytics_set(ANALYTICS_APP_METRIC_RESOURCE_TIMESTAMP, resource_version.timestamp, AnalyticsClient_App);
|
||||
|
||||
if (app_md->is_rocky_app) {
|
||||
analytics_inc(ANALYTICS_DEVICE_METRIC_APP_ROCKY_LAUNCH_COUNT, AnalyticsClient_System);
|
||||
analytics_inc(ANALYTICS_APP_METRIC_ROCKY_LAUNCH_COUNT, AnalyticsClient_App);
|
||||
}
|
||||
|
||||
if (launch_reason == APP_LAUNCH_QUICK_LAUNCH) {
|
||||
analytics_inc(ANALYTICS_DEVICE_METRIC_APP_QUICK_LAUNCH_COUNT, AnalyticsClient_System);
|
||||
analytics_inc(ANALYTICS_APP_METRIC_QUICK_LAUNCH_COUNT, AnalyticsClient_App);
|
||||
} else if (launch_reason == APP_LAUNCH_USER) {
|
||||
analytics_inc(ANALYTICS_DEVICE_METRIC_APP_USER_LAUNCH_COUNT, AnalyticsClient_System);
|
||||
analytics_inc(ANALYTICS_APP_METRIC_USER_LAUNCH_COUNT, AnalyticsClient_App);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
/*!
|
||||
@brief User mode access to its UUID.
|
||||
@param[out] uuid The app's UUID.
|
||||
*/
|
||||
DEFINE_SYSCALL(void, sys_get_app_uuid, Uuid *uuid) {
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
syscall_assert_userspace_buffer(uuid, sizeof(*uuid));
|
||||
}
|
||||
|
||||
*uuid = app_manager_get_current_app_md()->uuid;
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(Version, sys_get_current_app_sdk_version, void) {
|
||||
return process_metadata_get_sdk_version(app_manager_get_current_app_md());
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(bool, sys_get_current_app_is_js_allowed, void) {
|
||||
return (app_manager_get_current_app_md()->allow_js);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(bool, sys_get_current_app_is_rocky_app, void) {
|
||||
return (app_manager_get_current_app_md()->is_rocky_app);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(PlatformType, sys_get_current_app_sdk_platform, void) {
|
||||
return process_metadata_get_app_sdk_platform(app_manager_get_current_app_md());
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(bool, sys_app_is_watchface, void) {
|
||||
return app_manager_is_watchface_running();
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(ResAppNum, sys_get_current_resource_num, void) {
|
||||
|
||||
if (pebble_task_get_current() == PebbleTask_KernelMain) {
|
||||
return SYSTEM_APP;
|
||||
}
|
||||
|
||||
return process_metadata_get_res_bank_num(app_manager_get_current_app_md());
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(AppInstallId, sys_app_manager_get_current_app_id, void) {
|
||||
return app_manager_get_current_app_id();
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue