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,186 @@
/*
* 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_custom_icon.h"
#include "app_install_manager_private.h"
#include "pebble_process_info.h"
#include "apps/system_app_ids.h"
#include "kernel/pbl_malloc.h"
#include "services/common/comm_session/session.h"
#include "services/common/i18n/i18n.h"
#include "system/logging.h"
#include "util/attributes.h"
#include "util/math.h"
#include <string.h>
#include <stddef.h>
// We no longer have icons in the launcher, so we don't really need this anymore.
// However, we may decide to put it back in, so let's keep the code around just in case.
#define ALLOW_SET_ICON 0
typedef enum {
SPORTS = 0x00,
GOLF = 0x01,
NUM_APP_TYPES,
UNKNOWN_APP_TYPE,
APP_TYPE_MASK = 0x1,
} CustomizableAppType;
typedef enum {
NAME_FIELD = 0x00,
ICON_FIELD = 0x80,
FIELD_MASK = 0x80,
} FieldId;
typedef struct PACKED {
//! OR'ed value of (CustomizableAppType | FieldId). No C bitfields here, because order is compiler-specific.
uint8_t app_type_and_field_bits;
union {
char name[0];
struct {
uint16_t row_size_bytes;
uint16_t info_flags; //<! See GBitmap::info_flags
GRect bounds;
uint8_t image_data[];
} icon;
};
} AppCustomizeMessage;
typedef struct {
char *name;
GBitmap icon;
uint8_t *image_data;
CustomizableAppType app_type:4;
} AppCustomizeInfo;
static AppCustomizeInfo s_info[NUM_APP_TYPES];
static void do_callbacks(const CustomizableAppType app_type) {
if (app_type == UNKNOWN_APP_TYPE) {
return;
}
AppInstallId app_id = (app_type == SPORTS) ? APP_ID_SPORTS : APP_ID_GOLF;
app_install_do_callbacks(APP_ICON_NAME_UPDATED, app_id, NULL, NULL, NULL);
}
#if ALLOW_SET_ICON
static void set_icon(const CustomizableAppType app_type, const uint16_t row_size_bytes,
const uint16_t info_flags, const GRect bounds,
const uint8_t image_data[], const uint16_t image_data_length) {
AppCustomizeInfo *info = &s_info[app_type];
const uint16_t desired_size = row_size_bytes * bounds.size.h;
const uint16_t available_size = MIN(desired_size, image_data_length);
if (info->image_data &&
memcmp(info->image_data, image_data, available_size) == 0 &&
info->icon.bounds.origin.x == bounds.origin.x &&
info->icon.bounds.origin.y == bounds.origin.y &&
info->icon.bounds.size.w == bounds.size.w &&
info->icon.bounds.size.h == bounds.size.h &&
info->icon.info_flags == info_flags &&
info->icon.row_size_bytes == row_size_bytes) {
// Already equal to current icon
return;
}
if (info->image_data) {
kernel_free(info->image_data);
}
info->image_data = (uint8_t *) kernel_malloc(available_size);
memcpy(info->image_data, image_data, available_size);
info->icon.addr = info->image_data;
info->icon.bounds = bounds;
info->icon.row_size_bytes = row_size_bytes;
info->icon.info_flags = info_flags;
do_callbacks(app_type);
}
#endif
static void set_name(const CustomizableAppType app_type, const char *name, const uint16_t length) {
AppCustomizeInfo *info = &s_info[app_type];
if (info->name &&
strlen(info->name) == length &&
strncmp(info->name, name, length) == 0) {
// Already equal to current name
return;
}
if (info->name) {
kernel_free(info->name);
}
info->name = (char *) kernel_malloc(length + 1);
memcpy(info->name, name, length);
info->name[length] = 0;
do_callbacks(app_type);
}
void customizable_app_protocol_msg_callback(CommSession *session, const uint8_t* data, size_t length) {
AppCustomizeMessage *message = (AppCustomizeMessage *) data;
const FieldId field_id = message->app_type_and_field_bits & FIELD_MASK;
const CustomizableAppType app_type = message->app_type_and_field_bits & APP_TYPE_MASK;
switch (field_id) {
case NAME_FIELD: {
const uint16_t name_length = length - offsetof(AppCustomizeMessage, name);
set_name(app_type, message->name, name_length);
break;
}
#if ALLOW_SET_ICON
case ICON_FIELD: {
const uint16_t image_data_length = length - offsetof(AppCustomizeMessage, icon.image_data);
set_icon(app_type, message->icon.row_size_bytes, message->icon.info_flags, message->icon.bounds, message->icon.image_data, image_data_length);
break;
}
#endif
default: return;
}
(void)session;
}
static CustomizableAppType get_app_type_for_app_id(AppInstallId app_id) {
if (app_id == APP_ID_SPORTS) {
return SPORTS;
} else if (app_id == APP_ID_GOLF) {
return GOLF;
}
return UNKNOWN_APP_TYPE;
}
// Retrieve the custom name or return NULL
const char *app_custom_get_title(AppInstallId app_id) {
CustomizableAppType app_type = get_app_type_for_app_id(app_id);
const char *name = NULL;
if (app_type != UNKNOWN_APP_TYPE && s_info[app_type].name != NULL) {
name = s_info[app_type].name;
} else {
return NULL;
}
return name;
}
const GBitmap *app_custom_get_icon(AppInstallId app_id) {
CustomizableAppType app_type = get_app_type_for_app_id(app_id);
if (app_type != UNKNOWN_APP_TYPE && s_info[app_type].image_data != NULL) {
return &s_info[app_type].icon;
}
return NULL;
}

View file

@ -0,0 +1,30 @@
/*
* 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
//! @file app_custom_icon.h
//! This file is part of app_install_manager.c
//! It provides a Pebble protocol endpoint to allow 3rd party apps
//! to customize the title and icon of certain stock apps, like the "Sports" app.
#include "pebble_process_md.h"
#include "applib/graphics/gtypes.h"
#include "process_management/app_install_types.h"
const char *app_custom_get_title(AppInstallId app_id);
const GBitmap *app_custom_get_icon(AppInstallId app_id);

View file

@ -0,0 +1,893 @@
/*
* 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_install_manager.h"
#include "app_install_manager_private.h"
#include "app_custom_icon.h"
#include "app_manager.h"
#include "worker_manager.h"
#include "applib/event_service_client.h"
#include "apps/system_app_registry.h"
#include "console/prompt.h"
#include "drivers/task_watchdog.h"
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "kernel/pebble_tasks.h"
#include "kernel/util/sleep.h"
#include "resource/resource.h"
#include "resource/resource_ids.auto.h"
#include "services/common/comm_session/app_session_capabilities.h"
#include "services/common/i18n/i18n.h"
#include "services/normal/app_cache.h"
#include "services/normal/blob_db/app_db.h"
#include "services/normal/blob_db/pin_db.h"
#include "services/normal/persist.h"
#include "services/normal/process_management/app_storage.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/circular_cache.h"
#include "util/size.h"
#include <os/mutex.h>
#include <util/attributes.h>
typedef struct PACKED RecentApp {
AppInstallId id;
time_t last_activity;
bool can_expire;
} RecentApp;
// The number of applications to store in the circular cache.
// These are used to detect which application shave recently communicated
#define NUM_RECENT_APPS 5
#define CACHE_ENTRY_SIZE (sizeof(RecentApp))
#define CACHE_BUFFER_SIZE (NUM_RECENT_APPS * CACHE_ENTRY_SIZE)
#define RECENT_APP_LAST_ACTIVITY_INVALID (0)
typedef struct RecentAppCache {
PebbleRecursiveMutex *mutex;
CircularCache cache;
uint8_t cache_buffer[CACHE_BUFFER_SIZE];
} RecentAppCache;
static RecentAppCache s_recent_apps;
//! timeout for an app that has OnCommunication visibility (given in seconds)
static int32_t VISIBILITY_ON_ACTIVITY_TIMEOUT_SECONDS = (5 * SECONDS_PER_MINUTE);
static bool prv_app_install_entry_from_app_db_entry(AppInstallId id, AppDBEntry *db_entry,
AppInstallEntry *install_entry);
static AppInstallId s_pending_app_deletion;
static AppInstallId s_pending_worker_deletion;
// PBL-31769: This should be moved to send_text.c
#if !PLATFORM_TINTIN && defined(APP_ID_SEND_TEXT)
static EventServiceInfo s_capabilities_event_info;
#endif
//////////////////
// Misc helpers
//////////////////
static const AppRegistryEntry *prv_get_registry_list_entry(AppInstallId id,
unsigned int *record_order_out) {
if (app_install_id_from_app_db(id)) {
return NULL;
}
for (int i = 0; i < (int)ARRAY_LENGTH(APP_RECORDS); i++) {
if (APP_RECORDS[i].id == id) {
if (record_order_out) {
*record_order_out = i + 1;
}
return &APP_RECORDS[i];
}
}
return NULL;
}
// optimization: sort the UUID's and then search more quickly. This will most likely
// require many changes to the JSON generation script.
AppInstallId app_get_install_id_for_uuid_from_registry(const Uuid *uuid) {
for (uint16_t i = 0; i < ARRAY_LENGTH(APP_RECORDS); i++) {
const AppRegistryEntry *reg_entry = &APP_RECORDS[i];
if (reg_entry->type == AppInstallStorageFw) {
const PebbleProcessMd *md = reg_entry->md_fn();
if (md && uuid_equal(&md->uuid, uuid)) {
return reg_entry->id;
}
} else if ((reg_entry->type == AppInstallStorageResources)
&& uuid_equal(&reg_entry->uuid, uuid)) {
return reg_entry->id;
}
}
return INSTALL_ID_INVALID;
}
bool app_install_is_prioritized(AppInstallId install_id) {
if (install_id == INSTALL_ID_INVALID) {
return false;
}
bool rv = false;
mutex_lock_recursive(s_recent_apps.mutex);
{
RecentApp *app = circular_cache_get(&s_recent_apps.cache, &install_id);
if (app) {
const int32_t time_since_activity = time_get_uptime_seconds() - app->last_activity;
if (app->can_expire && (time_since_activity < VISIBILITY_ON_ACTIVITY_TIMEOUT_SECONDS)) {
// The recent app should eventually expire and we are still below the threshold
rv = true;
} else if (!app->can_expire && app->last_activity != RECENT_APP_LAST_ACTIVITY_INVALID) {
// The recent app should never expire and we haven't been manually expired yet.
rv = true;
}
}
}
mutex_unlock_recursive(s_recent_apps.mutex);
return rv;
}
void app_install_unmark_prioritized(AppInstallId install_id) {
if (install_id == INSTALL_ID_INVALID) {
return;
}
mutex_lock_recursive(s_recent_apps.mutex);
{
RecentApp *app = circular_cache_get(&s_recent_apps.cache, &install_id);
if (app) {
app->last_activity = RECENT_APP_LAST_ACTIVITY_INVALID;
}
}
mutex_unlock_recursive(s_recent_apps.mutex);
}
void app_install_mark_prioritized(AppInstallId install_id, bool can_expire) {
if (install_id == INSTALL_ID_INVALID) {
return;
}
mutex_lock_recursive(s_recent_apps.mutex);
{
const time_t cur_time = time_get_uptime_seconds();
RecentApp *app = circular_cache_get(&s_recent_apps.cache, &install_id);
if (app) {
app->last_activity = cur_time;
app->can_expire = can_expire;
} else {
RecentApp app = {
.id = install_id,
.last_activity = cur_time,
.can_expire = can_expire,
};
circular_cache_push(&s_recent_apps.cache, &app);
}
}
mutex_unlock_recursive(s_recent_apps.mutex);
}
#if UNITTEST
void app_install_manager_flush_recent_communication_timestamps(void) {
circular_cache_flush(&s_recent_apps.cache);
memset(&s_recent_apps.cache_buffer, 0, sizeof(s_recent_apps.cache));
}
#endif
bool app_install_entry_is_watchface(const AppInstallEntry *entry) {
return (entry->process_type == ProcessTypeWatchface);
}
bool app_install_entry_has_worker(const AppInstallEntry *entry) {
return (entry->has_worker);
}
bool app_install_entry_is_hidden(const AppInstallEntry *entry) {
switch (entry->visibility) {
case ProcessVisibilityHidden:
return true;
case ProcessVisibilityShownOnCommunication:
// make icon hidden (return true) if app has not recently communicated
return !app_install_is_prioritized(entry->install_id);
case ProcessVisibilityShown:
return false;
case ProcessVisibilityQuickLaunch:
return true;
}
return false;
}
bool app_install_entry_is_quick_launch_visible_only(const AppInstallEntry *entry) {
return (entry->visibility == ProcessVisibilityQuickLaunch);
}
bool app_install_entry_is_SDK_compatible(const AppInstallEntry *entry) {
return (entry->sdk_version.major == PROCESS_INFO_CURRENT_SDK_VERSION_MAJOR &&
entry->sdk_version.minor <= PROCESS_INFO_CURRENT_SDK_VERSION_MINOR);
}
T_STATIC ListNode *s_head_callback_node_list = NULL;
void app_install_register_callback(struct AppInstallCallbackNode *callback_node) {
PBL_ASSERTN(callback_node->node.next == NULL);
PBL_ASSERTN(callback_node->node.prev == NULL);
PBL_ASSERTN(s_head_callback_node_list != &callback_node->node);
PBL_ASSERTN(callback_node->callbacks != NULL);
callback_node->registered_by = pebble_task_get_current();
s_head_callback_node_list = list_prepend(s_head_callback_node_list, &callback_node->node);
}
void app_install_deregister_callback(struct AppInstallCallbackNode *callback_node) {
PBL_ASSERTN(callback_node->node.next != NULL
|| callback_node->node.prev != NULL
|| s_head_callback_node_list == &callback_node->node);
list_remove(&callback_node->node, &(s_head_callback_node_list), NULL);
}
void app_install_cleanup_registered_app_callbacks(void) {
struct AppInstallCallbackNode *iter = (struct AppInstallCallbackNode *) s_head_callback_node_list;
while (iter) {
if (iter->registered_by == PebbleTask_App) {
list_remove((ListNode *)&iter->node, &s_head_callback_node_list, NULL);
}
iter = (struct AppInstallCallbackNode *) list_get_next(&iter->node);
}
}
static void app_install_invoke_callbacks(InstallEventType event_type, AppInstallId install_id) {
struct AppInstallCallbackNode *callback_node = (struct AppInstallCallbackNode *) s_head_callback_node_list;
while (callback_node) {
if (callback_node->callbacks[event_type]) {
callback_node->callbacks[event_type](install_id, callback_node->data);
}
callback_node = (struct AppInstallCallbackNode *) list_get_next(&callback_node->node);
}
}
typedef struct {
AppInstallEnumerateCb cb;
void *data;
AppInstallEntry *entry_buf;
} EnumerateData;
static void prv_app_install_enumerate_app_db(AppInstallId install_id, AppDBEntry *db_entry,
void *data) {
EnumerateData *cb_data = (EnumerateData *)data;
prv_app_install_entry_from_app_db_entry(install_id, db_entry, cb_data->entry_buf);
cb_data->cb(cb_data->entry_buf, cb_data->data);
}
void app_install_enumerate_entries(AppInstallEnumerateCb cb, void *data) {
// Keep this off of the stack. This function presses the limits of our stack.
AppInstallEntry *entry = kernel_malloc_check(sizeof(AppInstallEntry));
// Iterate over the registry
for (uint32_t i = 0; i < ARRAY_LENGTH(APP_RECORDS); i++) {
if (app_install_get_entry_for_install_id(APP_RECORDS[i].id, entry)) {
// if a false is returned from the function, then stop iterating.
if (cb(entry, data) == false) {
kernel_free(entry);
return;
}
}
}
// Iterate over AppDB applications
EnumerateData cb_data = {
.cb = cb,
.data = data,
.entry_buf = entry,
};
app_db_enumerate_entries(prv_app_install_enumerate_app_db, &cb_data);
kernel_free(entry);
}
AppInstallId app_install_get_id_for_uuid(const Uuid *uuid) {
if (uuid_is_invalid(uuid) || uuid_is_system(uuid)) {
// Don't allow lookups by system uuid, there will be a bunch of apps with that uuid
return INSTALL_ID_INVALID;
}
// search in system registry first, if found return the ID.
AppInstallId id = app_get_install_id_for_uuid_from_registry(uuid);
if (id != INSTALL_ID_INVALID) {
return id;
}
// registry miss, now lets search in the app_db
id = app_db_get_install_id_for_uuid(uuid);
if (id != INSTALL_ID_INVALID) {
return id;
}
return INSTALL_ID_INVALID;
}
static void prv_app_install_delete(AppInstallId id, Uuid *uuid, bool app_upgrade,
bool delete_cache) {
if (!app_upgrade) {
// remove settings associated with the app
pin_db_delete_with_parent(uuid);
}
if (delete_cache) {
// only log when we actually delete the cache entry. This is so we don't print out 100 logs
// during an app cache clear
PBL_LOG(LOG_LEVEL_INFO, "Deleting app with id %"PRId32"", id);
app_cache_remove_entry(id);
}
}
static void prv_delete_pending_id(AppInstallId *app_id) {
if (*app_id != INSTALL_ID_INVALID) {
// app cache will delete the app binaries even if the entry for the app_id does not exist
app_cache_remove_entry(*app_id);
*app_id = INSTALL_ID_INVALID;
}
}
static void prv_process_pending_deletions(void) {
prv_delete_pending_id(&s_pending_app_deletion);
prv_delete_pending_id(&s_pending_worker_deletion);
PBL_LOG(LOG_LEVEL_DEBUG, "Finished processing pending deletions");
}
typedef struct InstallCallbackData {
//! We can't have multiple callbacks in flight at once. Only invoke a new set of callbacks
//! if this is false.
bool callback_in_progress;
//! We may have to pause doing callbacks to wait for the app or worker to close. If so, this is set to
//! true.
bool callback_paused_for_app;
bool callback_paused_for_worker;
InstallEventType install_type;
AppInstallId install_id;
Uuid *uuid;
//! Callback to call when we're doing issuing this callback.
InstallCallbackDoneCallback done_callback;
void* callback_data;
} InstallCallbackData;
InstallCallbackData s_install_callback_data;
static bool prv_ids_equal(AppInstallId one, AppInstallId two) {
return ((one == two) && (one != INSTALL_ID_INVALID));
}
static void app_install_launcher_task_callback(void *context) {
if (!s_install_callback_data.callback_paused_for_app &&
!s_install_callback_data.callback_paused_for_worker) {
// Only close the app the first time around.
if (s_install_callback_data.install_type == APP_UPGRADED ||
s_install_callback_data.install_type == APP_REMOVED ||
s_install_callback_data.install_type == APP_DB_CLEARED) {
const AppInstallId to_kill = s_install_callback_data.install_id;
// Close the current app if it is the one we are trying to remove/upgrade
// OR
// If we are doing an APP_DB_CLEAR and the currently running app is from the app_db,
// also clear it.
const AppInstallId cur_app_id = app_manager_get_current_app_id();
if (prv_ids_equal(cur_app_id, to_kill) ||
((s_install_callback_data.install_type == APP_DB_CLEARED) &&
(app_install_id_from_app_db(cur_app_id)))) {
PBL_LOG(LOG_LEVEL_DEBUG, "close and delay callbacks for app closing");
s_install_callback_data.callback_paused_for_app = true;
s_pending_app_deletion = cur_app_id;
app_manager_close_current_app(true);
}
// Close the current worker if it is the one we are trying to remove/upgrade
// OR
// If we are doing an APP_DB_CLEAR and the currently running worker is from the app_db,
// also clear it.
const AppInstallId cur_worker_id = worker_manager_get_current_worker_id();
if (prv_ids_equal(cur_worker_id, to_kill) ||
((s_install_callback_data.install_type == APP_DB_CLEARED) &&
(app_install_id_from_app_db(cur_worker_id)))) {
PBL_LOG(LOG_LEVEL_DEBUG, "close and delay callbacks for worker closing");
s_install_callback_data.callback_paused_for_worker = true;
s_pending_worker_deletion = cur_worker_id;
worker_manager_handle_remove_current_worker();
}
if (s_install_callback_data.callback_paused_for_app ||
s_install_callback_data.callback_paused_for_worker) {
// We're trying to remove or upgrade our currently running app. We now have
// to wait until the app actually closes before continuing to notify the rest
// of the system that we've removed or upgraded the app.
return;
}
}
}
PBL_LOG(LOG_LEVEL_DEBUG, "app_install_invoke_callbacks");
app_install_invoke_callbacks(s_install_callback_data.install_type,
s_install_callback_data.install_id);
bool app_upgrade = false;
switch (s_install_callback_data.install_type) {
case APP_UPGRADED:
app_upgrade = true;
/* fallthrough */
case APP_REMOVED:
prv_app_install_delete(s_install_callback_data.install_id, s_install_callback_data.uuid,
app_upgrade, true /* delete cache entry */);
// Only delete the app's persist file when the user explicitly removes the
// app, not during an AppDB clear.
if (!app_upgrade) {
persist_service_delete_file(s_install_callback_data.uuid);
comm_session_app_session_capabilities_evict(s_install_callback_data.uuid);
}
break;
case APP_DB_CLEARED:
prv_process_pending_deletions();
break;
default:
break;
}
if (s_install_callback_data.done_callback) {
s_install_callback_data.done_callback(s_install_callback_data.callback_data);
}
if (s_install_callback_data.uuid) {
kernel_free(s_install_callback_data.uuid);
}
s_install_callback_data = (InstallCallbackData) {
.callback_in_progress = false
};
}
bool app_install_do_callbacks(InstallEventType event_type, AppInstallId install_id,
Uuid *uuid, InstallCallbackDoneCallback done_callback, void* callback_data) {
if (s_install_callback_data.callback_in_progress) {
PBL_LOG(LOG_LEVEL_ERROR, "Failed to do app callbacks, already in progress");
return false;
}
s_install_callback_data = (InstallCallbackData) {
.callback_in_progress = true,
.install_id = install_id,
.uuid = uuid,
.install_type = event_type,
.done_callback = done_callback,
.callback_data = callback_data
};
launcher_task_add_callback(app_install_launcher_task_callback, NULL);
return true;
}
const char *app_install_get_custom_app_name(AppInstallId install_id) {
const char *name = app_custom_get_title(install_id);
if (name) {
return name;
}
return NULL;
}
uint32_t app_install_entry_get_icon_resource_id(const AppInstallEntry *entry) {
return entry->icon_resource_id;
}
ResAppNum app_install_get_app_icon_bank(const AppInstallEntry *entry) {
if (app_install_id_from_system(entry->install_id)) {
return SYSTEM_APP;
} else {
return entry->install_id;
}
}
bool app_install_is_app_running(AppInstallId id) {
return app_manager_get_task_context()->install_id == id;
}
bool app_install_is_worker_running(AppInstallId id) {
return worker_manager_get_task_context()->install_id == id;
}
void app_install_notify_app_closed(void) {
PBL_ASSERT_TASK(PebbleTask_KernelMain);
// If we've previously paused doing app callbacks to wait for the app to close, resume them
// now if the worker is also done
if (s_install_callback_data.callback_paused_for_app) {
if (!s_install_callback_data.callback_paused_for_worker) {
app_install_launcher_task_callback(NULL);
} else {
s_install_callback_data.callback_paused_for_app = false;
}
}
}
void app_install_notify_worker_closed(void) {
PBL_ASSERT_TASK(PebbleTask_KernelMain);
// If we've previously paused doing app callbacks to wait for the app to close, resume them
// now if the worker is also done
if (s_install_callback_data.callback_paused_for_worker) {
if (!s_install_callback_data.callback_paused_for_app) {
app_install_launcher_task_callback(NULL);
} else {
s_install_callback_data.callback_paused_for_worker = false;
}
}
}
//////////////////
// 3.0 Functions
//////////////////
static int prv_cmp_recent_apps(void *a, void *b) {
RecentApp *app_a = (RecentApp *)a;
RecentApp *app_b = (RecentApp *)b;
return !(app_a->id == app_b->id);
}
// PBL-31769: This should be moved to send_text.c
#if !PLATFORM_TINTIN && defined(APP_ID_SEND_TEXT)
static void prv_capabilities_changed_event_handler(PebbleEvent *event, void *context) {
// We only care if send text support changed right now
if (!event->capabilities.flags_diff.send_text_support) {
return;
}
const PebbleProcessMd *md = app_install_get_md(APP_ID_SEND_TEXT, false /* worker */);
const InstallEventType event_type = (md ? APP_AVAILABLE : APP_REMOVED);
app_install_invoke_callbacks(event_type, APP_ID_SEND_TEXT);
app_install_release_md(md);
}
#endif
void app_install_manager_init(void) {
circular_cache_init(&s_recent_apps.cache, s_recent_apps.cache_buffer, sizeof(RecentApp),
NUM_RECENT_APPS, prv_cmp_recent_apps);
s_recent_apps.mutex = mutex_create_recursive();
// PBL-31769: This should be moved to send_text.c
#if !PLATFORM_TINTIN && defined(APP_ID_SEND_TEXT)
s_capabilities_event_info = (EventServiceInfo) {
.type = PEBBLE_CAPABILITIES_CHANGED_EVENT,
.handler = prv_capabilities_changed_event_handler,
};
event_service_client_subscribe(&s_capabilities_event_info);
#endif
}
bool app_install_id_from_system(AppInstallId id) {
return (id < INSTALL_ID_INVALID);
}
bool app_install_id_from_app_db(AppInstallId id) {
return (id > INSTALL_ID_INVALID);
}
static GColor prv_hard_coded_color_for_3rd_party_apps(Uuid *uuid) {
// Remove this from Recovery FW for code size savings.
#if !defined(RECOVERY_FW) && !defined(PLATFORM_TINTIN)
// this is a temporary solution to enable custom colors for 3rd-party apps
// please replace this, once PBL-19673 landed
typedef struct {
Uuid uuid;
uint8_t color_argb;
} ColorMapping;
static const ColorMapping mappings[] = {
#include "app_install_manager_known_apps.h"
};
for (size_t i = 0; i < ARRAY_LENGTH(mappings); i++) {
if (uuid_equal(uuid, &mappings[i].uuid)) {
return (GColor){.argb = mappings[i].color_argb};
}
}
#endif
return GColorClear;
}
static GColor prv_valid_color_from_uuid(GColor color, Uuid *uuid) {
#if PLATFORM_TINTIN || PLATFORM_SILK
return GColorClear;
#endif
color = gcolor_closest_opaque(color);
if (!gcolor_equal(color, GColorClear)) {
return color;
}
color = prv_hard_coded_color_for_3rd_party_apps(uuid);
if (!gcolor_equal(color, GColorClear)) {
return color;
}
// if color isn't provided, build hash over uuid and pick from selected fall-back colors
GColor fall_back_colors[] = {GColorFromHEX(0x0000aa), GColorFromHEX(0x005500),
GColorFromHEX(0x550055), GColorFromHEX(0xff0055), GColorFromHEX(0xaa0000)};
uint8_t uuid_byte_sum = 0;
for (uint8_t *b = &uuid->byte0; b <= &uuid->byte15; b++) {
uuid_byte_sum += *b;
}
return fall_back_colors[uuid_byte_sum % ARRAY_LENGTH(fall_back_colors)];
}
static bool prv_app_install_entry_from_app_db_entry(AppInstallId id, AppDBEntry *db_entry,
AppInstallEntry *entry) {
*entry = (AppInstallEntry) {
.install_id = id,
.type = AppInstallStorageFlash,
.visibility = process_metadata_flags_visibility(db_entry->info_flags),
// PebbleTask_App because the flag parsing function needs it, and we can assume all
// applications registered with the manager are applications, not workers.
.process_type = process_metadata_flags_process_type(db_entry->info_flags, PebbleTask_App),
.has_worker = process_metadata_flags_has_worker(db_entry->info_flags),
.icon_resource_id = db_entry->icon_resource_id,
.uuid = db_entry->uuid,
.color = prv_valid_color_from_uuid(db_entry->app_face_bg_color, (Uuid *)&db_entry->uuid),
.sdk_version = db_entry->sdk_version,
};
strncpy(entry->name, db_entry->name, APP_NAME_SIZE_BYTES);
return true;
}
static bool prv_app_install_entry_from_resource_registry_entry(const AppRegistryEntry *reg_entry,
AppInstallEntry *entry) {
PebbleProcessInfo *app_header = kernel_malloc_check(sizeof(PebbleProcessInfo));
bool rv = false;
if (resource_load_byte_range_system(SYSTEM_APP, reg_entry->bin_resource_id, 0,
(uint8_t *)app_header, sizeof(*app_header)) != sizeof(*app_header)) {
PBL_LOG(LOG_LEVEL_WARNING, "Stored app with resource id %d not found in resources",
reg_entry->bin_resource_id);
goto done;
}
*entry = (AppInstallEntry) {
.install_id = reg_entry->id,
.type = AppInstallStorageResources,
.visibility = process_metadata_flags_visibility(app_header->flags),
// PebbleTask_App because the flag parsing function needs it, and we can assume all
// applications registered with the manager are applications, not workers.
.process_type = process_metadata_flags_process_type(app_header->flags, PebbleTask_App),
.has_worker = process_metadata_flags_has_worker(app_header->flags),
.icon_resource_id = reg_entry->icon_resource_id,
.uuid = reg_entry->uuid,
.color = prv_valid_color_from_uuid(reg_entry->color, (Uuid *) &reg_entry->uuid),
.sdk_version = app_header->sdk_version,
};
i18n_get_with_buffer(reg_entry->name, entry->name, APP_NAME_SIZE_BYTES);
rv = true;
done:
kernel_free(app_header);
return rv;
}
bool prv_app_install_entry_from_fw_registry_entry(const AppRegistryEntry *reg_entry,
AppInstallEntry *entry) {
const PebbleProcessMdSystem *md = (PebbleProcessMdSystem *) reg_entry->md_fn();
if (!md) {
return false;
}
*entry = (AppInstallEntry) {
.install_id = reg_entry->id,
.type = AppInstallStorageFw,
.visibility = md->common.visibility,
.process_type = md->common.process_type,
.has_worker = md->common.has_worker,
.icon_resource_id = md->icon_resource_id,
.uuid = md->common.uuid,
.color = prv_valid_color_from_uuid(reg_entry->color, (Uuid *) &md->common.uuid),
.sdk_version = process_metadata_get_sdk_version((PebbleProcessMd *)md),
};
i18n_get_with_buffer(md->name, entry->name, APP_NAME_SIZE_BYTES);
return true;
}
bool app_install_get_entry_for_install_id(AppInstallId install_id, AppInstallEntry *entry) {
if ((install_id == INSTALL_ID_INVALID) || (entry == NULL)) {
return false;
}
unsigned int record_order = 0;
const AppRegistryEntry *reg_entry = prv_get_registry_list_entry(install_id, &record_order);
if (reg_entry) {
bool rv = false;
// switch on registry type
switch (reg_entry->type) {
case AppInstallStorageFw:
rv = prv_app_install_entry_from_fw_registry_entry(reg_entry, entry);
break;
case AppInstallStorageResources:
rv = prv_app_install_entry_from_resource_registry_entry(reg_entry, entry);
break;
case AppInstallStorageInvalid:
case AppInstallStorageFlash:
PBL_LOG(LOG_LEVEL_DEBUG, "Invalid app registry type %d", reg_entry->type);
break;
}
if (rv) {
entry->record_order = record_order;
}
return rv;
} else if (app_db_exists_install_id(install_id)) {
AppDBEntry *db_entry = kernel_malloc_check(sizeof(AppDBEntry));
bool rv = (app_db_get_app_entry_for_install_id(install_id, db_entry) == S_SUCCESS);
if (rv) {
rv = prv_app_install_entry_from_app_db_entry(install_id, db_entry, entry);
}
kernel_free(db_entry);
return rv;
}
PBL_LOG(LOG_LEVEL_ERROR, "Failed to get entry for id %"PRId32, install_id);
return false;
}
bool app_install_get_uuid_for_install_id(AppInstallId install_id, Uuid *uuid_out) {
PBL_ASSERTN(uuid_out);
AppInstallEntry entry;
if (app_install_get_entry_for_install_id(install_id, &entry)) {
*uuid_out = entry.uuid;
return true;
} else {
*uuid_out = UUID_INVALID;
return false;
}
}
bool app_install_is_watchface(AppInstallId app_id) {
AppInstallEntry entry;
if (!app_install_get_entry_for_install_id(app_id, &entry)) {
return false;
}
return app_install_entry_is_watchface(&entry);
}
static const PebbleProcessMd *prv_get_md_for_reg_entry(const AppRegistryEntry *reg_entry) {
switch (reg_entry->type) {
case AppInstallStorageFw:
// If its a FW app, just return the Md
return reg_entry->md_fn();
case AppInstallStorageResources: {
// If its a RESOURCE app, we much read from the resource pack and populate an Md
PebbleProcessInfo app_header;
if (resource_load_byte_range_system(SYSTEM_APP, reg_entry->bin_resource_id, 0,
(uint8_t *)&app_header, sizeof(app_header)) != sizeof(app_header)) {
PBL_LOG(LOG_LEVEL_WARNING, "Stored app with resource id %d not found in resources",
reg_entry->bin_resource_id);
return NULL;
}
// Convert to PebbleProcessMd. Set the correct icon_id from the passed in argument
app_header.icon_resource_id = reg_entry->icon_resource_id;
// freed in process_manager.c
PebbleProcessMdResource *md = kernel_malloc_check(sizeof(PebbleProcessMdResource));
process_metadata_init_with_resource_header(md, &app_header, reg_entry->bin_resource_id,
PebbleTask_App);
const PebbleProcessMd *const_md = (PebbleProcessMd *)md;
return const_md;
}
default:
return NULL;
}
}
static const PebbleProcessMd *prv_get_md_for_flash_id(AppInstallId id, bool worker) {
#ifdef RECOVERY_FW
return NULL;
#endif
PebbleProcessInfo app_header;
uint8_t build_id_buffer[BUILD_ID_EXPECTED_LEN];
const PebbleTask task = worker ? PebbleTask_Worker : PebbleTask_App;
if (GET_APP_INFO_SUCCESS !=
app_storage_get_process_info(&app_header, build_id_buffer, id, task)) {
PBL_LOG(LOG_LEVEL_WARNING, "Failed to get app from flash with id %"PRIu32, id);
return NULL;
}
// freed in process_manager.c
PebbleProcessMdFlash *md = kernel_malloc_check(sizeof(PebbleProcessMdFlash));
process_metadata_init_with_flash_header(md, &app_header, id, task, build_id_buffer);
const PebbleProcessMd *const_md = (PebbleProcessMd *)md;
return const_md;
}
// PebbleProcessMd is freed in process_manager.c when the application quits
const PebbleProcessMd *app_install_get_md(AppInstallId id, bool worker) {
const AppRegistryEntry *reg_entry = prv_get_registry_list_entry(id, NULL /* record_order */);
if (reg_entry) {
return prv_get_md_for_reg_entry(reg_entry);
} else if (app_db_exists_install_id(id)) {
return prv_get_md_for_flash_id(id, worker);
}
// Not a registered app, fail.
PBL_LOG(LOG_LEVEL_ERROR, "Can't get PebbleProcessMd for app id %"PRId32, id);
return NULL;
}
void app_install_release_md(const PebbleProcessMd *md) {
if (!md) {
return;
}
switch (md->process_storage) {
case ProcessStorageBuiltin:
break;
case ProcessStorageFlash:
case ProcessStorageResource:
kernel_free((PebbleProcessMd*) md);
}
}
static void prv_enumerate_app_db_delete(AppInstallId install_id, AppDBEntry *db_entry,
void *data) {
PBL_ASSERTN(app_install_id_from_app_db(install_id));
task_watchdog_bit_set(pebble_task_get_current());
const bool gracefully = true;
if (app_manager_get_current_app_id() == install_id) {
process_manager_put_kill_process_event(PebbleTask_App, gracefully);
}
if (worker_manager_get_current_worker_id() == install_id) {
process_manager_put_kill_process_event(PebbleTask_Worker, gracefully);
}
// We are not deleting the cache here because it will be deleted quicker in filesystem iteration
// This way, it can clean up much quicker than searching through the filesystem every time
const bool app_upgrade = false;
const bool delete_cache = false;
prv_app_install_delete(install_id, &db_entry->uuid, app_upgrade, delete_cache);
}
void app_install_clear_app_db(void) {
app_db_enumerate_entries(prv_enumerate_app_db_delete, NULL);
app_cache_flush();
}

View file

@ -0,0 +1,264 @@
/*
* 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 "app_install_types.h"
#include "applib/graphics/gtypes.h"
#include "applib/graphics/gcolor_definitions.h"
#include "drivers/rtc.h"
#include "kernel/events.h"
#include "pebble_process_md.h"
#include "resource/resource.h"
#include "util/list.h"
//////////////////////////////////////////////////////////////////////////////
// This module is responsible for keeping track of what apps are installed,
// and acts as the abstraction which makes system and third party apps look the same
//////////////////////////////////////////////////////////////////////////////
//! There are many different representations, structures, and identifiers of an Application within
//! our firmware. This is an attempt to clear up the different types and also to document them.
//!
//! Accessing apps or their metadata
//! ----------------------------------
//! Preferred Methods
//!
//! - AppInstallEntry: Universal struct that contains all metadata for an application no matter
//! where it originates from (FW, Resource, Flash)
//! - AppInstallId: A number assigned to an application. Pre-assigned and negative for FW and
//! resource applications, assigned on install for Flash applications.
//!
//! Deprecated Methods
//!
//! - Uuid: a 16-byte identifier for an application. Every application process must contain a Uuid,
//! but it is not a requirement for firmware applications. Should avoid whenever possible.
//! A Uuid should only be used when in communication with the phone, since the phone should
//! have no knowledge of AppInstallId's.
//!
//! - PebbleProcessMd: This should only be used when launching an application. No piece of code
//! should ask the app_install_manager for a PebbleProcessMd unlesss it plans on
//! assisting with the launch of the application (event_loop, app_manager, etc.)
//!
//! How applications are stored
//! ---------------------------
//! Serialized Data Structures
//!
//! - AppDBEntry: The phone sends over this packed data in BlobDB and is stored in flash on the
//! watch. This data gets deserialized in the AppInstallManager and morphed into an
//! AppInstallEntry. This structure can't change without agreeing with the phone on
//! the changes. These entries are assigned an AppInstallId on install.
//!
//! - AppRegistryEntry: Hardcoded metadata for system application that comes packaged with the
//! firmware. There are two types: FW and RESOURCE. These entries are assigned
//! an AppInstallId by the programmer. Hardcoded and should never change.
//! - FW: These are the applications that have hard coded PebbleProcessMd's.
//! - RESOURCE: These are stored in the System Resource Pack and loaded on demand.
//!
//! Misc Notes
//! ----------
//!
//! If any module wants information about an application, the process is as follows:
//! 1. It should first retrieve an AppInstallId.
//! 2. Call app_install_get_entry_for_install_id and get the entry data structure
//! 3. Use the getter functions for the entry to retrieve individual fields within the struct.
#define TIMESTAMP_INVALID ((RtcTicks)0) //!< for most_recent_communication_timestamp in AppInstallEntry
//! Max number of bytes for an application.
#define APP_NAME_SIZE_BYTES 96
typedef enum {
AppInstallStorageInvalid = 0,
AppInstallStorageFw = 1,
AppInstallStorageFlash = 2,
AppInstallStorageResources = 3,
} AppInstallStorage;
typedef struct {
AppInstallId install_id;
AppInstallStorage type:2; // SYSTEM/RESOURCE/FLASH
ProcessVisibility visibility;
ProcessType process_type; // WATCHFACE/APP
bool has_worker;
Uuid uuid;
GColor color;
char name[APP_NAME_SIZE_BYTES];
int icon_resource_id;
Version sdk_version;
unsigned int record_order; //!< 0 means not in the app registry
} AppInstallEntry;
typedef enum {
APP_AVAILABLE = 0, //< occurs on app installation
APP_REMOVED = 1, //< occurs on app removal
APP_ICON_NAME_UPDATED = 2, //< occurs when app (metadata) has been updated
APP_UPGRADED = 3, //< occurs when app is getting removed prior to upgrade
APP_DB_CLEARED = 4, //< occurs when app is getting removed prior to upgrade
NUM_INSTALL_EVENT_TYPES,
} InstallEventType;
//! Used for the static application entries in the app registry
typedef const PebbleProcessMd* (*MdFunc) (void);
//! Used for apps listed in the system app registry
typedef struct {
AppInstallId id;
AppInstallStorage type;
GColor color;
union {
MdFunc md_fn;
struct {
const char *name;
Uuid uuid;
int bin_resource_id;
int icon_resource_id;
};
};
} AppRegistryEntry;
void app_install_manager_init(void);
//! Get AppInstallId for the provided Uuid
//! @param uuid Uuid to convert to an AppInstallId
//! @return valid AppInstallId or INSTALL_ID_INVALID
AppInstallId app_install_get_id_for_uuid(const Uuid *uuid);
//! Search the system registry for the AppInstallId for the provided Uuid
//! @param uuid Uuid to convert to an AppInstallId
//! @return valid AppInstallId or INSTALL_ID_INVALID if not a built-in app
AppInstallId app_get_install_id_for_uuid_from_registry(const Uuid *uuid);
//! Subscription callback
typedef void (*AppInstallCallback)(const AppInstallId install_id, void *data);
typedef struct AppInstallCallbackNode {
ListNode node;
//! Must point to data that lives at least until app_install_deregister_callback() is called:
void *data;
//! Array of callbacks for each event type:
const AppInstallCallback *callbacks;
PebbleTask registered_by;
} AppInstallCallbackNode;
//! Registers callbacks for add/remove/change events from the app install manager
//! @note Callbacks are invoked on the launcher task!
void app_install_register_callback(struct AppInstallCallbackNode *callback_info);
//! Deregisters callbacks for add/remove/change events from the app install manager
void app_install_deregister_callback(struct AppInstallCallbackNode *callback_info);
//! Deregisters callbacks that were registered on the app task
void app_install_cleanup_registered_app_callbacks(void);
//! Generates an AppInstallEntry for the install_id given and reads it into the given entry
//! pointer.
//!
//! @note If any piece of code wants to read any characteristics of an installed application,
//! it should first get an entry then call the below operations to read the fields of the
//! struct.
//! @param install_id AppInstallId of the application
//! @param entry AppInstallEntry buffer to write to
//! @return return True if the entry was successfully retrieved and written to the buffer. Will
//! return false if the ID is invalid or does not exist, or if entry is NULL.
bool app_install_get_entry_for_install_id(AppInstallId install_id, AppInstallEntry *entry);
//! Gets the corresponding Uuid given an AppInstallId. This loads an entry to obtain its
//! information, use the entry directly if your context has an AppInstallEntry.
//! @param install_id AppInstallId of the application
//! @param uuid_out pointer to a Uuid buffer where the application Uuid will be written
//! @return false if the ID is invalid or does not exist, true otherwise
bool app_install_get_uuid_for_install_id(AppInstallId install_id, Uuid *uuid_out);
//! Gets whether the app is a watchface given an AppInstallId. This loads an entry to obtain its
//! information, use \ref app_install_entry_is_watchface if your context has an AppInstallEntry.
//! @param install_id AppInstallId of the application
//! @return true if the app is a watchface, false otherwise
bool app_install_is_watchface(AppInstallId app_id);
//! Returns true if the app associated with the provided entry is a watchface
//! @param entry AppInstallEntry to check the parameters of
bool app_install_entry_is_watchface(const AppInstallEntry *entry);
//! Returns true if the app associated with the provided entry has a worker
//! @param entry AppInstallEntry to check the parameters of
bool app_install_entry_has_worker(const AppInstallEntry *entry);
//! Returns true if the app associated with the provided entry should be hidden in menus
//! @param entry AppInstallEntry to check the parameters of
bool app_install_entry_is_hidden(const AppInstallEntry *entry);
//! Gets whether the app is visible in the list of apps that can be set as a quick launch shortcut.
//! @return true if the app is only visible in quick launch, false otherwise
bool app_install_entry_is_quick_launch_visible_only(const AppInstallEntry *entry);
//! Returns true if the app associated with the provided entry is SDK compatible
//! @param entry AppInstallEntry to check the parameters of
bool app_install_entry_is_SDK_compatible(const AppInstallEntry *entry);
typedef bool(*AppInstallEnumerateCb)(AppInstallEntry *entry, void *data);
//! Enumerates all active install ids for non-hidden apps and calls the given function for each
//! install id.
//! @param cb Pointer to a function that will get called once for each app install id.
//! This callback can return false to end the enumeration prematurely, or true to continue.
//! @param data Pointer to arbitrary user data that will get passed into the function
void app_install_enumerate_entries(AppInstallEnumerateCb cb, void *data);
void app_install_clear_app_db(void);
//! These functions are not reading characteristics of the AppInstallEntry so they do not
//! require an entry to be passed in.
bool app_install_is_app_running(AppInstallId id);
bool app_install_is_worker_running(AppInstallId id);
void app_install_notify_app_closed(void);
void app_install_notify_worker_closed(void);
//! @param id The ID for the desired MD
//! @param worker True if we want the worker MD, false if we want the app MD
//! @return Returns a pointer to the PebbleProcessMd. When the caller is done with this pointer
//! they should call app_install_md_release to release the associated memory.
const PebbleProcessMd *app_install_get_md(AppInstallId id, bool worker);
//! Release a md that was previously allocated by app_install_get_md.
void app_install_release_md(const PebbleProcessMd *md);
//! Retrieves the custom name for an application if it has sent a new application name
const char *app_install_get_custom_app_name(AppInstallId install_id);
bool app_install_is_prioritized(AppInstallId install_id);
void app_install_mark_prioritized(AppInstallId install_id, bool can_expire);
void app_install_unmark_prioritized(AppInstallId install_id);
#if UNITTEST
void app_install_manager_flush_recent_communication_timestamps(void);
#endif
//////////////////////
// Deprecated
// = Will remove once launcher is reimplemented
//////////////////////
uint32_t app_install_entry_get_icon_resource_id(const AppInstallEntry *entry);
ResAppNum app_install_get_app_icon_bank(const AppInstallEntry *entry);

View file

@ -0,0 +1,83 @@
/*
* 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.
*/
// @nolint
// please don't change these values manually, they are derived from the spreadsheet
// "2015 Partner Planning - list of color for apps" from the bizdev team
{.uuid = {0xe0, 0x89, 0x86, 0x19, 0xec, 0xcd, 0x43, 0x70, 0x91, 0x41, 0x2c, 0xe1, 0x9b, 0x91, 0xc4, 0x32}, .color_argb = GColorRedARGB8}, // Yelp
{.uuid = {0xd6, 0xdb, 0xcb, 0x9f, 0x06, 0x14, 0x48, 0x98, 0x99, 0x8c, 0xb7, 0xb6, 0x3e, 0x7c, 0xe5, 0x7c}, .color_argb = GColorRajahARGB8}, // Swarm
{.uuid = {0xe0, 0x89, 0x86, 0x19, 0xec, 0xcd, 0x43, 0x70, 0x91, 0x41, 0x2c, 0xe1, 0x9b, 0x91, 0xc4, 0x32}, .color_argb = GColorIslamicGreenARGB8}, // Evernote
{.uuid = {0xc5, 0xfb, 0xbd, 0x64, 0x9a, 0xae, 0x43, 0x5a, 0xa2, 0x9c, 0x1c, 0x99, 0xf0, 0x72, 0xa8, 0xf3}, .color_argb = GColorBlueMoonARGB8}, // eBay
{.uuid = {0x7b, 0x3f, 0x56, 0x49, 0xf0, 0x3a, 0x42, 0xf9, 0x9d, 0x86, 0xbe, 0x21, 0xb0, 0x47, 0x49, 0xa5}, .color_argb = GColorCobaltBlueARGB8}, // Pandora
{.uuid = {0x0b, 0x73, 0xb7, 0x6a, 0xcd, 0x65, 0x4d, 0xc2, 0x95, 0x85, 0xaa, 0xa2, 0x13, 0x32, 0x08, 0x58}, .color_argb = GColorBlackARGB8}, // Misfit
{.uuid = {0x7f, 0x97, 0x08, 0xf8, 0x9b, 0x15, 0x47, 0x57, 0x99, 0x18, 0xea, 0x3a, 0x4c, 0x85, 0x63, 0xa5}, .color_argb = GColorPictonBlueARGB8}, // Swim.com
{.uuid = {0x80, 0x89, 0x03, 0xcb, 0xfa, 0x37, 0x42, 0x47, 0xb0, 0x12, 0x7d, 0xfe, 0x5c, 0x92, 0x69, 0xe1}, .color_argb = GColorOrangeARGB8}, // Pixel Miner
{.uuid = {0x2e, 0xdf, 0xf9, 0xdf, 0x4d, 0x60, 0x47, 0xdb, 0x99, 0x62, 0x43, 0x7d, 0x40, 0xc5, 0xf1, 0xe2}, .color_argb = GColorLibertyARGB8}, // Tiny Bird
{.uuid = {0x17, 0xb3, 0x32, 0x24, 0x31, 0x7d, 0x44, 0xd7, 0x9e, 0x70, 0x62, 0x80, 0xd4, 0x2f, 0xb9, 0x39}, .color_argb = GColorMidnightGreenARGB8}, // PinyWings
{.uuid = {0x43, 0x6f, 0xe4, 0x62, 0x4a, 0xa0, 0x4c, 0xa3, 0xbd, 0x2c, 0x2c, 0x8e, 0x72, 0xb0, 0x32, 0x81}, .color_argb = GColorOrangeARGB8}, // Cards for Pebble
{.uuid = {0xdc, 0x36, 0x23, 0xce, 0xc8, 0x04, 0x4a, 0xba, 0x8b, 0xe4, 0xf0, 0x52, 0x73, 0xee, 0x87, 0x74}, .color_argb = GColorDarkGrayARGB8}, // Smartwatch+
{.uuid = {0x4b, 0x76, 0x00, 0x64, 0x14, 0x88, 0x40, 0x44, 0x96, 0x7a, 0x1b, 0x1d, 0x3a, 0xb3, 0x05, 0x74}, .color_argb = GColorDarkGrayARGB8}, // Glance
{.uuid = {0x24, 0xb0, 0xa1, 0xe5, 0xf0, 0xb1, 0x44, 0x0d, 0x8e, 0x53, 0x8f, 0x26, 0x68, 0x8a, 0x3f, 0x07}, .color_argb = GColorKellyGreenARGB8}, // Sleep as Android
{.uuid = {0xe3, 0x06, 0x00, 0x14, 0xbf, 0xe9, 0x4d, 0x03, 0xb7, 0x5e, 0x37, 0x4c, 0xc4, 0xdc, 0xa9, 0x7b}, .color_argb = GColorRajahARGB8}, // Alarms++
{.uuid = {0x54, 0x3e, 0xc8, 0xb0, 0xa9, 0x4b, 0x4f, 0xbb, 0x95, 0x31, 0x63, 0x47, 0x84, 0x2f, 0xcd, 0xb1}, .color_argb = GColorVividCeruleanARGB8}, // Timer (3rd party)
{.uuid = {0x1b, 0xdf, 0xe4, 0x35, 0x6a, 0x34, 0x42, 0xd5, 0xae, 0xd7, 0xac, 0xe2, 0x9f, 0xec, 0x12, 0x59}, .color_argb = GColorCadetBlueARGB8}, // NavMe
{.uuid = {0x4e, 0x81, 0x8f, 0xcd, 0x7d, 0xaf, 0x4d, 0x09, 0xba, 0x51, 0x60, 0x8a, 0x2a, 0x44, 0x98, 0x11}, .color_argb = GColorElectricUltramarineARGB8}, // LetsMuv
{.uuid = {0x7e, 0x4b, 0x6a, 0x11, 0xd4, 0x20, 0x46, 0x01, 0xa5, 0xd3, 0xeb, 0x44, 0x09, 0x39, 0x6a, 0x6f}, .color_argb = GColorKellyGreenARGB8}, // Endomondo
{.uuid = {0xf3, 0x8e, 0x8e, 0xf7, 0xf2, 0x10, 0x4b, 0x9a, 0xbf, 0xb3, 0x67, 0x26, 0xe2, 0x93, 0x4e, 0x09}, .color_argb = GColorPictonBlueARGB8}, // Movable
{.uuid = {0x58, 0x5f, 0x47, 0xed, 0x99, 0x97, 0x4e, 0x32, 0xbd, 0x94, 0x67, 0xb9, 0xc3, 0x64, 0xa5, 0xec}, .color_argb = GColorBlackARGB8}, // Mr Runner - Up
{.uuid = {0x09, 0x33, 0x25, 0xba, 0x1b, 0xe9, 0x4f, 0x5f, 0xba, 0x80, 0xb4, 0x75, 0x04, 0x75, 0x41, 0x91}, .color_argb = GColorSunsetOrangeARGB8}, // WW - Weather Watch
{.uuid = {0xfb, 0x53, 0x38, 0xd6, 0x75, 0x1c, 0x4d, 0x4f, 0x90, 0x74, 0x70, 0xd4, 0xba, 0xd0, 0x21, 0xa0}, .color_argb = GColorLightGrayARGB8}, // SmartStatus
{.uuid = {0x5b, 0xe4, 0x4f, 0x1d, 0xd2, 0x62, 0x4e, 0xa6, 0xaa, 0x30, 0xdd, 0xbe, 0xc1, 0xe3, 0xca, 0xb2}, .color_argb = GColorDukeBlueARGB8}, // Morpheuz
{.uuid = {0x5f, 0xac, 0x58, 0x89, 0x26, 0x70, 0x46, 0x43, 0x82, 0x57, 0xb0, 0x73, 0x87, 0x7d, 0x63, 0xce}, .color_argb = GColorIndigoARGB8}, // Awear
{.uuid = {0x48, 0x18, 0xa8, 0xbd, 0x6e, 0x53, 0x49, 0x1a, 0xb6, 0xb5, 0x63, 0x02, 0xb3, 0x36, 0x3d, 0x9b}, .color_argb = GColorSpringBudARGB8}, // Battery Lifetime
{.uuid = {0xe4, 0x16, 0x8f, 0x6c, 0xb4, 0x85, 0x4b, 0x6f, 0x99, 0xc4, 0x94, 0xaa, 0x95, 0x7e, 0x86, 0xd5}, .color_argb = GColorMalachiteARGB8}, // Music Boss
{.uuid = {0x0d, 0xb6, 0xa5, 0x5e, 0xb3, 0x2a, 0x4b, 0x03, 0xb0, 0x37, 0x95, 0x63, 0x7b, 0xf3, 0x06, 0xff}, .color_argb = GColorBlackARGB8}, // MultiTimer
{.uuid = {0x4d, 0x6e, 0xa3, 0xee, 0x0f, 0x2a, 0x4c, 0x33, 0xb0, 0x42, 0xe2, 0xe5, 0x6c, 0xe8, 0x0c, 0xd4}, .color_argb = GColorPurpleARGB8}, // Yo
{.uuid = {0x93, 0x6a, 0x77, 0x26, 0x4c, 0x8f, 0x41, 0x67, 0x8c, 0x25, 0x46, 0x68, 0x36, 0x82, 0xb0, 0x6e}, .color_argb = GColorDarkGrayARGB8}, // Pebtris
{.uuid = {0x22, 0xe5, 0x53, 0x87, 0x92, 0x3b, 0x41, 0x9f, 0xae, 0x03, 0xad, 0x09, 0xff, 0x7e, 0xa2, 0x95}, .color_argb = GColorMelonARGB8}, // Calendar
{.uuid = {0x86, 0x5a, 0xd5, 0x5a, 0xe0, 0x18, 0x40, 0x70, 0x97, 0x07, 0x98, 0x22, 0x5b, 0x5a, 0x6a, 0x34}, .color_argb = GColorLightGrayARGB8}, // Asteriod
{.uuid = {0x7f, 0xb1, 0xc6, 0x61, 0x04, 0x50, 0x40, 0x92, 0xb4, 0x13, 0x5c, 0xc8, 0x40, 0xfa, 0x09, 0x45}, .color_argb = GColorBlackARGB8}, // 3 Calendar
{.uuid = {0x4c, 0x9b, 0x88, 0x5b, 0x40, 0x0d, 0x4f, 0x1c, 0x8e, 0x99, 0x4d, 0x38, 0xa1, 0xb7, 0x96, 0xb4}, .color_argb = GColorBlackARGB8}, // GoPro
{.uuid = {0x5c, 0xc7, 0xeb, 0xd1, 0xea, 0x97, 0x49, 0x4c, 0xae, 0x7c, 0xec, 0xd0, 0x5d, 0xd3, 0x3a, 0x5a}, .color_argb = GColorRedARGB8}, // Email to SMS
{.uuid = {0x6f, 0x93, 0x02, 0xfc, 0xed, 0x64, 0x43, 0x5a, 0xae, 0x1a, 0x83, 0x30, 0x8f, 0xe1, 0x18, 0x02}, .color_argb = GColorDarkCandyAppleRedARGB8}, // Pebble Snap
{.uuid = {0x12, 0x10, 0xb1, 0x65, 0x10, 0x52, 0x44, 0x7d, 0x80, 0xb1, 0x55, 0xce, 0x6e, 0x5d, 0x1b, 0x20}, .color_argb = GColorCobaltBlueARGB8}, // Dominos
{.uuid = {0xee, 0x54, 0x11, 0x34, 0x3b, 0x52, 0x4f, 0x7c, 0xbd, 0x88, 0x08, 0xc3, 0x27, 0x6c, 0xa1, 0x4b}, .color_argb = GColorRedARGB8}, // Telepizza
{.uuid = {0x5e, 0x6f, 0xa6, 0x42, 0x51, 0xe9, 0x4e, 0xaf, 0xae, 0x07, 0x7c, 0xdc, 0x27, 0x33, 0x0f, 0xf1}, .color_argb = GColorFollyARGB8}, // AirBerlin
{.uuid = {0xc9, 0x80, 0x84, 0x49, 0x03, 0x00, 0x4e, 0x33, 0x9a, 0xe4, 0xb1, 0x26, 0x5b, 0xaf, 0x42, 0xab}, .color_argb = GColorDarkGrayARGB8}, // Mercedes Benz
{.uuid = {0x58, 0x78, 0x14, 0xd7, 0x79, 0x15, 0x4e, 0x70, 0x8d, 0x77, 0xd5, 0x07, 0x18, 0x7c, 0xb4, 0x4e}, .color_argb = GColorPictonBlueARGB8}, // Gym Timer
{.uuid = {0x2c, 0x4a, 0x34, 0x23, 0x2e, 0x3e, 0x46, 0x0a, 0x8f, 0x6d, 0xbc, 0xdb, 0x4b, 0x46, 0x21, 0xb3}, .color_argb = GColorBlueMoonARGB8}, // Bart on Time
{.uuid = {0x00, 0xe9, 0xde, 0xeb, 0x16, 0xb4, 0x47, 0x52, 0xae, 0x35, 0x2c, 0xc0, 0x88, 0xfc, 0x6c, 0xa9}, .color_argb = GColorOxfordBlueARGB8}, // UK Transport
{.uuid = {0x43, 0xa4, 0xb4, 0xd6, 0x94, 0x04, 0x4f, 0x39, 0x86, 0x8c, 0x59, 0x48, 0x01, 0xa7, 0xb6, 0xb0}, .color_argb = GColorPictonBlueARGB8}, // Twebble
{.uuid = {0x7c, 0x62, 0xa8, 0x0d, 0xbf, 0x8f, 0x4c, 0xd0, 0x87, 0x9c, 0x14, 0xb7, 0x2f, 0x41, 0x07, 0x9d}, .color_argb = GColorWindsorTanARGB8}, // JavaPay
{.uuid = {0x98, 0xa1, 0xc9, 0x95, 0x74, 0x82, 0x48, 0x61, 0xbf, 0xcf, 0xb4, 0x44, 0x75, 0x6b, 0x77, 0xa3}, .color_argb = GColorLightGrayARGB8}, // PebbDrive
{.uuid = {0xf6, 0xf1, 0xa8, 0xdf, 0x4d, 0x06, 0x4e, 0x8a, 0xbe, 0x7c, 0x52, 0xfb, 0x32, 0xf5, 0x3f, 0x98}, .color_argb = GColorRedARGB8}, // PebbleCN
{.uuid = {0x03, 0xa8, 0x3a, 0x53, 0x94, 0x33, 0x49, 0xde, 0x89, 0xde, 0xa3, 0x0e, 0xed, 0x2b, 0xd5, 0x89}, .color_argb = GColorRedARGB8}, // PebbleCC
{.uuid = {0x1a, 0x1a, 0x0e, 0xa3, 0x15, 0x9b, 0x41, 0x97, 0x94, 0x37, 0x16, 0x24, 0xdd, 0x2f, 0xb0, 0x65}, .color_argb = GColorLimerickARGB8}, // PebbGPS
{.uuid = {0x49, 0x82, 0x77, 0x22, 0x4f, 0x3b, 0x4e, 0x3f, 0x9e, 0x96, 0xd6, 0x60, 0x08, 0x9f, 0x50, 0xc1}, .color_argb = GColorMidnightGreenARGB8}, // Compass
{.uuid = {0x3a, 0xc7, 0x96, 0xc0, 0x47, 0x5b, 0x4c, 0xdf, 0x99, 0x77, 0x86, 0x8f, 0xfd, 0x5d, 0x22, 0x25}, .color_argb = GColorDarkGrayARGB8}, // Pebblets
{.uuid = {0x17, 0xbf, 0x83, 0xaa, 0x88, 0xa9, 0x45, 0x9a, 0xa6, 0xe5, 0x60, 0xaa, 0x60, 0xe1, 0xdc, 0x07}, .color_argb = GColorWindsorTanARGB8}, // Timer+
{.uuid = {0xd9, 0x48, 0x17, 0x9c, 0x88, 0xf9, 0x4b, 0x15, 0xa8, 0x4a, 0x35, 0x64, 0x8e, 0x2a, 0x56, 0x10}, .color_argb = GColorVividCeruleanARGB8}, // Leaf
{.uuid = {0xd6, 0x2b, 0xc4, 0x67, 0x84, 0x99, 0x4a, 0xb5, 0x8b, 0xf1, 0x35, 0xeb, 0xf1, 0xee, 0x8f, 0x08}, .color_argb = GColorCadetBlueARGB8}, // Wrist Presenter
{.uuid = {0xe6, 0x0b, 0x30, 0x32, 0x3d, 0x91, 0x4c, 0xb8, 0x8f, 0xa7, 0x27, 0x49, 0x00, 0x0b, 0x7c, 0xa5}, .color_argb = GColorBlackARGB8}, // Kronos
{.uuid = {0x06, 0x0d, 0x81, 0x97, 0x9c, 0xf9, 0x49, 0xd4, 0xab, 0x1d, 0x03, 0x07, 0x4d, 0x0a, 0x6d, 0x50}, .color_argb = GColorOrangeARGB8}, // Grill Timer
{.uuid = {0x07, 0xd8, 0x78, 0x11, 0x51, 0x0f, 0x48, 0xf2, 0xb7, 0x23, 0x6b, 0xcf, 0xc4, 0xdb, 0x9a, 0x40}, .color_argb = GColorTiffanyBlueARGB8}, // Pedemeter
{.uuid = {0x0f, 0xca, 0x4e, 0x5c, 0xda, 0xf2, 0x4a, 0x9b, 0xa2, 0xbd, 0x53, 0xa7, 0x3f, 0x4b, 0x02, 0x1a}, .color_argb = GColorMayGreenARGB8}, // Swing by Swing
{.uuid = {0x73, 0x8f, 0xf8, 0x50, 0xe6, 0x64, 0x44, 0x38, 0x85, 0x34, 0x7f, 0x82, 0x9a, 0x3a, 0x07, 0x1d}, .color_argb = GColorOxfordBlueARGB8}, // Glympse
{.uuid = {0x40, 0xdf, 0x22, 0x7e, 0x47, 0x15, 0x43, 0x69, 0x82, 0xca, 0xec, 0x9d, 0xe5, 0x88, 0x89, 0xea}, .color_argb = GColorDarkCandyAppleRedARGB8}, // CalTrain
{.uuid = {0x1b, 0x35, 0x6b, 0xed, 0xdb, 0xbc, 0x41, 0xd5, 0xa0, 0xf3, 0xec, 0x60, 0xbd, 0x75, 0x1b, 0x15}, .color_argb = GColorDarkCandyAppleRedARGB8}, // ESPN
{.uuid = {0xc6, 0x4a, 0xae, 0x42, 0xb8, 0x22, 0x4d, 0x6d, 0x99, 0x5b, 0xbc, 0x7c, 0x38, 0xe8, 0x43, 0xf4}, .color_argb = GColorCelesteARGB8}, // Timer (1st party)
{.uuid = {0x14, 0xbd, 0x9c, 0xd3, 0x14, 0xdc, 0x4b, 0x81, 0xbd, 0x58, 0x2f, 0x1f, 0xb9, 0xb5, 0x79, 0x78}, .color_argb = GColorYellowARGB8}, // Stopwatch

View file

@ -0,0 +1,31 @@
/*
* 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 "app_install_manager.h"
#include "pebble_process_md.h"
//! @file app_install_manager_private.h
//! These are the "private" functions used by submodules of app_install_manager.
typedef void (*InstallCallbackDoneCallback)(void*);
//! Used by app_custom_icon and app_db, so invoke add/remove/update/app_db_clear callbacks.
//! This function takes care of calling the callbacks on the proper task, so this function
//! can be called from any task.
//! @return false if a callback is already in progress
bool app_install_do_callbacks(InstallEventType event_type, AppInstallId install_id, Uuid *uuid,
InstallCallbackDoneCallback done_callback, void* done_callback_data);

View file

@ -0,0 +1,33 @@
/*
* 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 <stdint.h>
#include <stdbool.h>
//! ID unique to a given app for the duration that it is installed
//! System apps (system/resource) and banked applications are negative numbers.
//! AppDB flash apps are positive numbers
typedef int32_t AppInstallId;
#define INSTALL_ID_INVALID ((AppInstallId)0)
//! Returns true for system applications
bool app_install_id_from_system(AppInstallId id);
//! Returns true for user installed applications
bool app_install_id_from_app_db(AppInstallId id);

View 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();
}

View file

@ -0,0 +1,121 @@
/*
* 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 "launch_config.h"
#include "process_manager.h"
#include "kernel/events.h"
#include "kernel/pebble_tasks.h"
#include "resource/resource.h"
#include "services/common/compositor/compositor.h"
#include <stdbool.h>
#define APP_TASK_PRIORITY (tskIDLE_PRIORITY + 2)
typedef enum {
AppTaskCtxIdxLauncher = 0,
AppTaskCtxIdxApp,
AppTaskCtxIdxCount,
AppTaskCtxIdxInvalid = -1,
} AppTaskCtxIdx;
typedef struct AppLaunchConfig {
LaunchConfigCommon common;
const PebbleProcessMd *md;
bool restart; //!< Allows the current app to be restarted
bool forcefully; //!< Causes the current app to be forcefully closed
} AppLaunchConfig;
typedef struct AppLaunchEventConfig {
LaunchConfigCommon common;
AppInstallId id;
} AppLaunchEventConfig;
// App management functions
void app_manager_init(void);
bool app_manager_is_initialized(void);
void app_manager_start_first_app(void);
bool app_manager_is_first_app_launched(void);
//! Start up a new application with the given metadata. This will kill the currently running app.
//! May only be called from the KernelMain task
bool app_manager_launch_new_app(const AppLaunchConfig *config);
//! Handles the PebbleEvent of type PEBBLE_APP_FETCH_REQUEST_EVENT, for example, by launching the
//! fetched app.
void app_manager_handle_app_fetch_request_event(const PebbleAppFetchRequestEvent *const evt);
//! Close the currently running app. This will cause the next app in the
//! system app state machine to automatically get launched.
//! May only be called from the KernelMain task
void app_manager_close_current_app(bool gracefully);
//! Stop the current app, and bring up the launcher.
void app_manager_force_quit_to_launcher(void);
//! Sets a minimum run level for app launches that interrupt the current running app. Any app below the specified
//! will be ignored when trying to launch.
//! The minimum run level will be reset to the incoming app whenever an app is launched. This function just allows
//! an app to modify the minimum level between app changes.
void app_manager_set_minimum_run_level(ProcessAppRunLevel run_level);
//! Gets the wakeup info from the PebbleEvent sent to app_manager
//! to provide newly launched app access to AppLaunchEvent data
WakeupInfo app_manager_get_app_wakeup_state(void);
//! @return Whether or not we support an app execution environment for this app.
bool app_manager_is_app_supported(const PebbleProcessMd *md);
// Send App Management Events To The Launcher
///////////////////////////////////////////////////////////////////////////////
//! Adds a PebbleLaunchAppEvent to the event queue
void app_manager_put_launch_app_event(const AppLaunchEventConfig *config);
// Getters For App Management State
///////////////////////////////////////////////////////////////////////////////
const PebbleProcessMd* app_manager_get_current_app_md(void);
AppInstallId app_manager_get_current_app_id(void);
const PebbleProcessMd* app_manager_get_current_worker(void);
ProcessContext* app_manager_get_task_context(void);
bool app_manager_is_watchface_running(void);
ResAppNum app_manager_get_current_resource_num(void);
AppLaunchReason app_manager_get_launch_reason(void);
ButtonId app_manager_get_launch_button(void);
void app_manager_get_framebuffer_size(GSize *size);
//! Exit the application. Do some cleanup to make sure things close nicely.
//! Called from the app task
NORETURN app_task_exit(void);

View file

@ -0,0 +1,544 @@
/*
* 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_menu_data_source.h"
#include "applib/fonts/fonts.h"
#include "applib/ui/menu_layer.h"
#include "apps/system_app_ids.h"
#include "kernel/pbl_malloc.h"
#include "process_management/app_manager.h"
#include "resource/resource_ids.auto.h"
#include "services/common/i18n/i18n.h"
#include "services/normal/process_management/app_order_storage.h"
#include "system/passert.h"
#include "util/size.h"
#include "apps/system_apps/activity_demo_app.h"
#include "shell/prefs.h"
#include <string.h>
static void add_app_with_install_id(const AppInstallEntry *entry, AppMenuDataSource *source);
static bool remove_app_with_install_id(const AppInstallId install_id, AppMenuDataSource *source);
static AppMenuNode * prv_find_node_with_install_id(const AppInstallId install_id,
const AppMenuDataSource * const source);
static void prv_unload_node(const AppMenuDataSource *source, AppMenuNode *node);
////////////////////////////////
// List helper functions
////////////////////////////////
static bool prv_is_app_filtered_out(AppInstallEntry *entry, AppMenuDataSource * const source) {
return (source->callbacks.filter && (source->callbacks.filter(source, entry) == false));
}
/////////////////////////
// Order List helpers
/////////////////////////
//! Place these in the order that is desired in the Launcher.
//! Set `move_on_activity` to true if you only want the item to jump to the top during communication
//! The movement will not happen while looking at the launcher, it will only refresh on a
//! close->open
static const struct {
AppInstallId install_id;
bool move_on_activity;
} s_override_table[] = {
{ APP_ID_SPORTS, false },
{ APP_ID_GOLF, false },
#ifdef APP_ID_WORKOUT
{ APP_ID_WORKOUT, true },
#endif
{ APP_ID_MUSIC, true },
};
// Returns 0 if not in table. Otherwise, returns the rank in `s_override_table`. Rank is
// where the lowest index returns the highest rank
static int prv_override_index(AppInstallId app_id) {
const uint32_t num_overrides = ARRAY_LENGTH(s_override_table);
for (uint32_t i = 0; i < num_overrides; i++) {
AppInstallId cur_id = s_override_table[i].install_id;
if (cur_id == app_id) {
const bool should_move = (!s_override_table[i].move_on_activity ||
app_install_is_prioritized(cur_id));
return (should_move) ? (num_overrides - i + 1) : 0;
}
}
return 0;
}
static int prv_app_override_comparator(AppInstallId app_id, AppInstallId new_id) {
return (prv_override_index(app_id) - prv_override_index(new_id));
}
static int prv_comparator_ascending_zero_last(unsigned int a, unsigned int b) {
return ((a != 0) && (b != 0)) ? (b - a) : // Sort in ascending order
(a - b); // 0 should be sorted last so invert the sort
}
T_STATIC int prv_app_node_comparator(void *app_node_ref, void *new_node_ref) {
const AppMenuNode *app_node = app_node_ref;
const AppMenuNode *new_node = new_node_ref;
const bool is_app_quick_launch = (app_node->visibility == ProcessVisibilityQuickLaunch);
const bool is_new_quick_launch = (new_node->visibility == ProcessVisibilityQuickLaunch);
const int override_cmp_rv = prv_app_override_comparator(app_node->install_id,
new_node->install_id);
if (is_app_quick_launch != is_new_quick_launch) {
// Quick Launch only apps are first
return (is_app_quick_launch ? 1 : 0) - (is_new_quick_launch ? 1 : 0);
} else if (override_cmp_rv) {
// Apps that override storage, record, and install order
return (override_cmp_rv);
} else if (app_node->storage_order != new_node->storage_order) {
// Storage order (smallest first)
return prv_comparator_ascending_zero_last(app_node->storage_order, new_node->storage_order);
} else if (app_node->record_order != new_node->record_order) {
// Record order (smallest first)
return prv_comparator_ascending_zero_last(app_node->record_order, new_node->record_order);
} else {
// AppInstallId (smallest first)
return new_node->install_id - app_node->install_id;
}
}
static void prv_set_storage_order(AppMenuDataSource *source, AppMenuNode *menu_node,
AppMenuOrderStorage *storage, bool update_and_take_ownership) {
if (!storage) {
return;
}
for (int i = 0; i < storage->list_length; i++) {
const AppInstallId storage_app_id = storage->id_list[i];
if (storage_app_id == INSTALL_ID_INVALID) {
continue;
}
const AppMenuStorageOrder new_storage_order =
(AppMenuStorageOrder)i + AppMenuStorageOrderGeneralOrderOffset;
if (menu_node && (menu_node->install_id == storage_app_id)) {
menu_node->storage_order = new_storage_order;
if (!update_and_take_ownership) {
break;
} else {
continue;
}
}
if (update_and_take_ownership) {
AppMenuNode *other_node = prv_find_node_with_install_id(storage_app_id, source);
if (other_node) {
other_node->storage_order = new_storage_order;
}
}
}
if (update_and_take_ownership) {
app_free(storage);
}
}
static void prv_sorted_add(AppMenuDataSource *source, AppMenuNode *menu_node) {
// Update the entire list order only if we've just read the order in this context. If we haven't
// just read the order, then we're building a list starting from an empty list, so just set the
// order for the new node.
prv_set_storage_order(source, menu_node, source->order_storage ?: app_order_read_order(),
(source->order_storage == NULL));
// If we're adding the Settings app node to the list and it hasn't received a storage order,
// then give it its default order
if ((menu_node->install_id == APP_ID_SETTINGS) &&
(menu_node->storage_order == AppMenuStorageOrder_NoOrder)) {
menu_node->storage_order = AppMenuStorageOrder_SettingsDefaultOrder;
}
source->list = (AppMenuNode *)list_sorted_add(&source->list->node, &menu_node->node,
prv_app_node_comparator, true /* ascending */);
}
////////////////////////////////
// AppInstallManager Callbacks
////////////////////////////////
typedef struct InstallData {
AppInstallId id;
AppMenuDataSource *source;
InstallEventType event_type;
} InstallData;
void prv_alert_data_source_changed(AppMenuDataSource *data_source) {
if (data_source->callbacks.changed) {
data_source->callbacks.changed(data_source->callback_context);
}
}
static void prv_do_app_added(AppMenuDataSource *source, AppInstallId install_id);
static void prv_do_app_removed(AppMenuDataSource *source, AppInstallId install_id);
static void prv_do_app_icon_name_updated(AppMenuDataSource *source, AppInstallId install_id);
static void prv_do_app_db_cleared(AppMenuDataSource *source);
void prv_handle_app_event(void *data) {
InstallData *install_data = data;
AppMenuDataSource *source = install_data->source;
AppInstallId install_id = install_data->id;
switch (install_data->event_type) {
case APP_AVAILABLE:
prv_do_app_added(source, install_id);
break;
case APP_REMOVED:
prv_do_app_removed(source, install_id);
break;
case APP_ICON_NAME_UPDATED:
prv_do_app_icon_name_updated(source, install_id);
break;
case APP_DB_CLEARED:
prv_do_app_db_cleared(source);
break;
default:
break;
}
kernel_free(install_data);
}
void prv_send_callback_to_app(AppMenuDataSource *data_source, AppInstallId install_id,
InstallEventType event_type) {
InstallData *install_data = kernel_malloc_check(sizeof(InstallData));
*install_data = (InstallData) {
.id = install_id,
.source = data_source,
.event_type = event_type,
};
process_manager_send_callback_event_to_process(PebbleTask_App,
prv_handle_app_event,
install_data);
}
//! Must be run from the app task
static void prv_do_app_added(AppMenuDataSource *source, AppInstallId install_id) {
AppInstallEntry entry;
if (!app_install_get_entry_for_install_id(install_id, &entry) ||
prv_is_app_filtered_out(&entry, source)) {
return;
}
add_app_with_install_id(&entry, source);
prv_alert_data_source_changed(source);
}
//! Called when an application is installed
static void prv_app_added_callback(const AppInstallId install_id, void *data) {
AppMenuDataSource *data_source = data;
prv_send_callback_to_app(data_source, install_id, APP_AVAILABLE);
}
//! Must be run from the app task
static void prv_do_app_removed(AppMenuDataSource *source, AppInstallId install_id) {
// Don't filter, just always try removing from the list:
const bool is_removed = remove_app_with_install_id(install_id, source);
if (is_removed) {
prv_alert_data_source_changed(source);
}
}
//! Called when an application is uninstalled
static void prv_app_removed_callback(const AppInstallId install_id, void *data) {
AppMenuDataSource *data_source = data;
prv_send_callback_to_app(data_source, install_id, APP_REMOVED);
}
//! Must be run from the app task
static void prv_do_app_icon_name_updated(AppMenuDataSource *source, AppInstallId install_id) {
AppInstallEntry entry;
if (!app_install_get_entry_for_install_id(install_id, &entry)) {
return;
}
AppMenuNode *node = prv_find_node_with_install_id(install_id, source);
if (prv_is_app_filtered_out(&entry, source)) {
if (node == NULL) {
// Changed and still excluded:
return;
}
// Changed and is now excluded:
prv_unload_node(source, node);
} else {
// Changed and is now included:
if (node == NULL) {
add_app_with_install_id(&entry, source);
}
}
prv_alert_data_source_changed(source);
}
static void prv_app_icon_name_updated_callback(const AppInstallId install_id, void *data) {
AppMenuDataSource *data_source = data;
prv_send_callback_to_app(data_source, install_id, APP_ICON_NAME_UPDATED);
}
//! Must be run from the app task
static void prv_do_app_db_cleared(AppMenuDataSource *source) {
AppMenuNode *iter = source->list;
while (iter) {
AppMenuNode *temp = (AppMenuNode *)list_get_next((ListNode *)iter);
// if the node belonged to the app_db, remove
if (app_install_id_from_app_db(iter->install_id)) {
prv_unload_node(source, iter);
}
iter = temp;
}
prv_alert_data_source_changed(source);
}
static void prv_app_db_cleared_callback(const AppInstallId install_id, void *data) {
// data is just a pointer to the AppMenuDataSource
prv_send_callback_to_app((AppMenuDataSource *)data, INSTALL_ID_INVALID, APP_DB_CLEARED);
}
static bool prv_app_enumerate_callback(AppInstallEntry *entry, void *data) {
if (prv_is_app_filtered_out(entry, (AppMenuDataSource *)data)) {
return true; // continue
}
add_app_with_install_id(entry, data);
return true; // continue
}
////////////////////
// Add / remove helper functions:
// This function should only be called once per app entry. The icon from the app will either be
// loaded and cached or we will load the default system icon that is set by the client.
static void prv_load_list_item_icon(AppMenuDataSource *source, AppMenuNode *node) {
// Should only call this function if the icon has not been loaded
PBL_ASSERTN(node->icon == NULL);
if (node->icon_resource_id != RESOURCE_ID_INVALID) {
// If we have some sort of valid resource_id, try loading it
node->icon = gbitmap_create_with_resource_system(node->app_num, node->icon_resource_id);
}
if (!node->icon) {
// If we failed to load the app's icon or it didn't have one, use the default. This will either
// be NULL or an actual icon...both are fine. And no need to clip the default icon
node->icon = source->default_icon;
return;
}
// Clip the icon down if needed
static const GRect icon_clip = {{0, 0}, {32, 32}};
grect_clip(&node->icon->bounds, &icon_clip);
}
static void prv_unload_list_item_icon(const AppMenuDataSource *source, AppMenuNode *node) {
// Don't destroy the default icon here, we'll destroy it later.
if (node->icon && node->icon != source->default_icon) {
gbitmap_destroy(node->icon);
node->icon = NULL;
}
}
static void prv_load_list_if_needed(AppMenuDataSource *source) {
if (source->is_list_loaded) {
return;
}
source->is_list_loaded = true;
PBL_ASSERTN(!source->order_storage);
source->order_storage = app_order_read_order();
app_install_enumerate_entries(prv_app_enumerate_callback, source);
app_free(source->order_storage);
source->order_storage = NULL;
}
static void prv_unload_node(const AppMenuDataSource *source, AppMenuNode *node) {
prv_unload_list_item_icon(source, node);
list_remove((ListNode*)node, (ListNode**)&source->list, NULL);
app_free(node->name);
app_free(node);
}
static void add_app_with_install_id(const AppInstallEntry *entry, AppMenuDataSource *source) {
if (source->is_list_loaded == false) {
return;
}
AppMenuNode *node = app_malloc_check(sizeof(AppMenuNode));
*node = (AppMenuNode) {
.install_id = entry->install_id,
.app_num = app_install_get_app_icon_bank(entry),
.icon_resource_id = app_install_entry_get_icon_resource_id(entry),
.uuid = entry->uuid,
.color = entry->color,
.visibility = entry->visibility,
.sdk_version = entry->sdk_version,
.record_order = entry->record_order,
};
uint8_t len;
const char *app_name = app_install_get_custom_app_name(node->install_id);
if (!app_name) {
app_name = entry->name;
}
len = strlen(app_name) + 1;
node->name = app_malloc_check(len);
strncpy(node->name, app_name, len);
prv_sorted_add(source, node);
}
static AppMenuNode * prv_find_node_with_install_id(const AppInstallId install_id,
const AppMenuDataSource * const source) {
AppMenuNode *node = source->list;
while (node != NULL) {
if (node->install_id == install_id) {
return node;
} else {
node = (AppMenuNode*)list_get_next((ListNode*)node);
}
}
return NULL;
}
//! @return True if there was app with install_id was found and removed from the list.
static bool remove_app_with_install_id(const AppInstallId install_id, AppMenuDataSource *source) {
if (source->is_list_loaded == false) {
return false;
}
AppMenuNode *node = prv_find_node_with_install_id(install_id, source);
if (node == NULL) {
return false;
}
prv_unload_node(source, node);
return true;
}
////////////////////
// public interface
void app_menu_data_source_init(AppMenuDataSource *source,
const AppMenuDataSourceCallbacks *callbacks,
void *callback_context) {
PBL_ASSERTN(source != NULL);
*source = (AppMenuDataSource) {
.callback_context = callback_context,
};
if (callbacks) {
source->callbacks = *callbacks;
}
// Register callbacks for app_install_manager updates:
static const AppInstallCallback s_app_install_callbacks[NUM_INSTALL_EVENT_TYPES] = {
[APP_AVAILABLE] = prv_app_added_callback,
[APP_REMOVED] = prv_app_removed_callback,
[APP_UPGRADED] = prv_app_removed_callback,
[APP_ICON_NAME_UPDATED] = prv_app_icon_name_updated_callback,
[APP_DB_CLEARED] = prv_app_db_cleared_callback,
};
source->app_install_callback_node = (struct AppInstallCallbackNode) {
.data = source,
.callbacks = s_app_install_callbacks,
};
app_install_register_callback(&source->app_install_callback_node);
}
void app_menu_data_source_deinit(AppMenuDataSource *source) {
app_install_deregister_callback(&source->app_install_callback_node);
// Free the AppMenuNodes:
AppMenuNode *node = source->list;
while (node != NULL) {
AppMenuNode * const next = (AppMenuNode*)list_get_next((ListNode*)node);
prv_unload_node(source, node);
node = next;
}
if (source->default_icon) {
gbitmap_destroy(source->default_icon);
}
source->callbacks.changed = NULL;
source->is_list_loaded = false;
}
void app_menu_data_source_enable_icons(AppMenuDataSource *source, uint32_t fallback_icon_id) {
// should only call this once, and should be passed in a valid resource id.
PBL_ASSERTN(source->default_icon == NULL && fallback_icon_id != RESOURCE_ID_INVALID);
source->show_icons = true;
// The return value will be a valid GBitmap* or NULL (because of an OOM that shouldn't ever happen
// We will handle both gracefully.
source->default_icon = gbitmap_create_with_resource_system(SYSTEM_APP, fallback_icon_id);
}
static uint16_t prv_transform_index(AppMenuDataSource *source, uint16_t index) {
if (source->callbacks.transform_index) {
return source->callbacks.transform_index(source, index, source->callback_context);
}
return index;
}
AppMenuNode* app_menu_data_source_get_node_at_index(AppMenuDataSource *source, uint16_t row_index) {
prv_load_list_if_needed(source);
return (AppMenuNode*)list_get_at((ListNode*)source->list,
prv_transform_index(source, row_index));
}
uint16_t app_menu_data_source_get_count(AppMenuDataSource *source) {
prv_load_list_if_needed(source);
return list_count((ListNode*)source->list);
}
uint16_t app_menu_data_source_get_index_of_app_with_install_id(AppMenuDataSource *source,
AppInstallId install_id) {
prv_load_list_if_needed(source);
AppMenuNode *node = source->list;
uint16_t index = 0;
while (node != NULL) {
if (node->install_id == install_id) {
return prv_transform_index(source, index);
}
node = (AppMenuNode*)list_get_next((ListNode*)node);
++index;
}
return MENU_INDEX_NOT_FOUND;
}
GBitmap *app_menu_data_source_get_node_icon(AppMenuDataSource *source, AppMenuNode *node) {
if (!node->icon && source->show_icons) {
// If the icon is currently NULL and we should be showing icons, load the icon
prv_load_list_item_icon(source, node);
}
// Will return the icon if it exists, or NULL if one doesn't.
return node->icon;
}
void app_menu_data_source_draw_row(AppMenuDataSource *source, GContext *ctx, Layer *cell_layer,
MenuIndex *cell_index) {
AppMenuNode *node = app_menu_data_source_get_node_at_index(source, cell_index->row);
// Will return an icon or NULL depending on if icons are enabled.
GBitmap *bitmap = app_menu_data_source_get_node_icon(source, node);
const GCompOp op = (gbitmap_get_format(bitmap) == GBitmapFormat1Bit) ? GCompOpTint : GCompOpSet;
graphics_context_set_compositing_mode(ctx, op);
menu_cell_basic_draw(ctx, cell_layer, node->name, NULL, bitmap);
}

View file

@ -0,0 +1,127 @@
/*
* 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
//!@file app_menu_data_source.h
//!
//! This file provides a utility for populating a MenuLayer with the apps that are currently installed. This should
//! only be used by system apps such as the Launcher or the Watchface Selector apps, as it integrates tightly with
//! app_install_manager.
#include "applib/ui/kino/kino_reel.h"
#include "applib/ui/menu_layer.h"
#include "os/mutex.h"
#include "process_management/app_install_manager.h"
#include "process_management/pebble_process_info.h"
#include "process_management/pebble_process_md.h"
#include "services/normal/process_management/app_order_storage.h"
#include "util/attributes.h"
#include "util/list.h"
struct AppMenuDataSource;
//! This enum provides special cases of app storage order and helps calculate the fixed offset at
//! which the general storage order should begin
typedef enum AppMenuStorageOrder {
AppMenuStorageOrder_NoOrder = 0,
AppMenuStorageOrder_SettingsDefaultOrder,
AppMenuStorageOrderGeneralOrderOffset
} AppMenuStorageOrder;
typedef struct PACKED AppMenuNode {
ListNode node;
AppInstallId install_id;
ResAppNum app_num;
uint32_t icon_resource_id;
GBitmap *icon;
Uuid uuid;
GColor color;
char *name;
ProcessVisibility visibility;
Version sdk_version;
unsigned int storage_order; //!< See \ref AppMenuStorageOrder for special values of this field
unsigned int record_order; //!< 0 means not in the app registry
} AppMenuNode;
// Clang makes the size of enums like AppMenuStorageOrder larger than the minimum size needed to
// represent all of the enum's values, so this assert doesn't make since when compiling using Clang
#if !__clang__
_Static_assert(
sizeof(((AppMenuNode *)0)->storage_order) >=
sizeof(((AppMenuOrderStorage *)0)->list_length) + sizeof(AppMenuStorageOrder),
"The size of AppMenuNode.storage_order must be at least as large as the combined size of "
"AppMenuOrderStorage.list_length and the AppMenuStorageOrder enum since the enum provides "
"additional values for AppMenuNode.storage_order.");
#endif
typedef bool (*AppMenuFilterCallback)(struct AppMenuDataSource *source, AppInstallEntry *entry);
typedef void (*AppMenuDataSourceFunc)(void *context);
typedef uint16_t (*AppMenuDataSourceIndexTransform)(struct AppMenuDataSource *source,
uint16_t index, void *context);
typedef struct AppMenuDataSourceCallbacks {
AppMenuFilterCallback filter;
AppMenuDataSourceFunc changed;
AppMenuDataSourceIndexTransform transform_index;
} AppMenuDataSourceCallbacks;
typedef struct AppMenuDataSource {
AppMenuNode *list;
AppMenuOrderStorage *order_storage;
AppInstallCallbackNode app_install_callback_node;
AppMenuDataSourceCallbacks callbacks;
void *callback_context;
GBitmap *default_icon;
bool show_icons;
bool is_list_loaded;
} AppMenuDataSource;
//! Initalize the AppMenuDataSource
void app_menu_data_source_init(AppMenuDataSource *source,
const AppMenuDataSourceCallbacks *handlers,
void *callback_context);
//! Deinitalize the AppMenuDataSource
void app_menu_data_source_deinit(AppMenuDataSource *source);
//! Will load the icons for each `AppMenuNode`. Will automatically be unloaded when
//! `app_menu_data_source_deinit` is called.
//! @param fallback_icon_id The fallback resource id that should be used if the entry does not have
//! an icon.
void app_menu_data_source_enable_icons(AppMenuDataSource *source, uint32_t fallback_icon_id);
//! Returns the AppMenuNode at the given index.
AppMenuNode* app_menu_data_source_get_node_at_index(AppMenuDataSource *source, uint16_t row_index);
//! Returns the AppMenuNode with the given AppInstallId.
uint16_t app_menu_data_source_get_index_of_app_with_install_id(AppMenuDataSource *source,
AppInstallId install_id);
//! Returns the count of AppMenuNode's in the source.
uint16_t app_menu_data_source_get_count(AppMenuDataSource *source);
//! Calls `menu_cell_basic_draw` for the AppMenuNode at the given MenuIndex
//! @note Will only draw an icon if `show_icons` is set to True.
void app_menu_data_source_draw_row(AppMenuDataSource *source, GContext *ctx, Layer *cell_layer,
MenuIndex *cell_index);
//! Returns the allocated `GBitmap *` for the given node. Will return NULL if no icon is loaded.
//! @note Will only return an icon if `show_icons` is set to True.
GBitmap *app_menu_data_source_get_node_icon(AppMenuDataSource *source, AppMenuNode *node);

View file

@ -0,0 +1,150 @@
/*
* 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_run_state.h"
#include "launcher_app_message.h"
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "process_management/app_install_manager.h"
#include "process_management/app_manager.h"
#include "process_management/process_manager.h"
#include "services/common/system_task.h"
#include "system/logging.h"
#include "util/attributes.h"
#define PB_APP_STATE_ENDPOINT_ID 0x34
typedef struct PACKED {
AppState state:8;
Uuid uuid;
} AppRunState;
static void prv_send_response(void *data) {
AppRunState *app_run_state = (AppRunState*)data;
CommSession *session = comm_session_get_system_session();
if (session) {
if (comm_session_has_capability(session, CommSessionRunState)) {
char uuid_buffer[UUID_STRING_BUFFER_LENGTH];
bool success = comm_session_send_data(session, PB_APP_STATE_ENDPOINT_ID,
(uint8_t*)app_run_state, sizeof(*app_run_state),
COMM_SESSION_DEFAULT_TIMEOUT);
uuid_to_string(&app_run_state->uuid, uuid_buffer);
PBL_LOG(LOG_LEVEL_DEBUG, "AppRunState(0x34) %s sending status: %s - %u",
(success ? "success" : "failed"), uuid_buffer, app_run_state->state);
} else {
PBL_LOG(LOG_LEVEL_DEBUG, "Using deprecated launcher_app_message");
const bool is_running = ((app_run_state->state == RUNNING) ? true : false);
launcher_app_message_send_app_state_deprecated(&app_run_state->uuid, is_running);
}
}
kernel_free(app_run_state);
}
void app_run_state_command(CommSession *session, AppRunStateCommand cmd, const Uuid *uuid) {
const AppInstallId install_id = app_install_get_id_for_uuid(uuid);
// Log most recent communication timestamp:
app_install_mark_prioritized(install_id, true /* can_expire */);
if (install_id == INSTALL_ID_INVALID && cmd != APP_RUN_STATE_STATUS_COMMAND) {
char uuid_buffer[UUID_STRING_BUFFER_LENGTH];
uuid_to_string(uuid, uuid_buffer);
PBL_LOG(LOG_LEVEL_DEBUG, "No app found with uuid %s", uuid_buffer);
return;
}
switch (cmd) {
case APP_RUN_STATE_RUN_COMMAND:
// Launch the application provided it isn't running, otherwise this is a noop
app_manager_put_launch_app_event(&(AppLaunchEventConfig) {
.id = install_id,
.common.reason = APP_LAUNCH_PHONE,
});
break;
case APP_RUN_STATE_STOP_COMMAND:
// Stop the application provided it is running, otherwise this is a noop
app_install_unmark_prioritized(install_id);
if (app_install_is_app_running(install_id)) {
process_manager_put_kill_process_event(PebbleTask_App, true);
}
break;
case APP_RUN_STATE_STATUS_COMMAND:
// Determine the running application
uuid = &app_manager_get_current_app_md()->uuid;
if (session != NULL) {
// We check the session here as to be backwards compatibile with the 0x31 endpoint and
// to avoid repeating code, the endpoint makes use of this function, but since it does
// not have an active session (it's session is NULL), it will fall to the else case.
AppRunState *app_run_state = kernel_malloc_check(sizeof(AppRunState));
app_run_state->state = RUNNING;
app_run_state->uuid = *uuid;
prv_send_response(app_run_state);
} else {
launcher_app_message_send_app_state_deprecated(uuid, RUNNING);
}
break;
default:
PBL_LOG(LOG_LEVEL_ERROR, "Unknown command: %d", cmd);
}
}
void app_run_state_protocol_msg_callback(CommSession *session, const uint8_t *data, size_t length) {
typedef struct PACKED {
uint8_t command;
Uuid uuid;
} AppStateMessage;
const AppStateMessage *msg = (const AppStateMessage*)data;
if (msg->command != APP_RUN_STATE_STATUS_COMMAND) {
if (length < sizeof(AppStateMessage)) {
PBL_LOG(LOG_LEVEL_ERROR, "length mismatch, expected %"PRIu32" byte(s), got %"PRIu32" bytes",
(uint32_t) sizeof(AppStateMessage), (uint32_t) length);
return;
}
}
app_run_state_command(session, msg->command, uuid_is_invalid(&msg->uuid) ? NULL : &msg->uuid);
}
void app_run_state_send_update(const Uuid *uuid, AppState app_state) {
#ifdef RECOVERY_FW
// FIXME: Need to actually factor out this so it's totally removed from PRF, but for now just
// pull it all. We can't use this here because we don't initialize app message at all.
return;
#endif
// This function deprecates the 0x31 launcher_app_message_send_app_state
// providing a different method of interacting with the endpoint. Calls the old
// method if the mobile application does not support the new endpoint.
CommSession *session = comm_session_get_system_session();
if (!session) {
// If we don't have a comm session open, don't bother sending application messages
return;
}
// Offload to KernelBG, because this function is called 2x when switching apps and we want to
// be sure not to block KernelMain for 2x 4000ms when the send buffer is full.
AppRunState *app_run_state = kernel_malloc(sizeof(AppRunState));
app_run_state->uuid = *uuid;
app_run_state->state = app_state;
system_task_add_callback(prv_send_response, app_run_state);
}

View file

@ -0,0 +1,45 @@
/*
* 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 "services/common/comm_session/session.h"
#include "util/uuid.h"
typedef enum {
//! Used as reply from the watch to the phone, to indicate the app is running.
//! Or, when pushed from phone to watch, this value will have the effect of launching the app.
RUNNING = 0x01,
//! Used as reply from the watch to the phone, to indicate the app is not running.
//! Or, when pushed from phone to watch, this value will have the effect of killing the app.
NOT_RUNNING = 0x02
} AppState;
typedef enum {
//! These keys (with accompanying UUID values self FETCH) can be pushed from the phone to
//! the watch to launch/kill an app on the watch or query which application is running.
//! Backwards compatible for support of deprecated 0x31
APP_RUN_STATE_INVALID_COMMAND = 0x00, // Invalid state key, used as a default value
APP_RUN_STATE_RUN_COMMAND = 0x01, // Watch -> Phone: App is running, Phone -> Watch: Start app
APP_RUN_STATE_STOP_COMMAND = 0x02, // Watch -> Phone: App is stopped, Phone -> Watch: Stop app
APP_RUN_STATE_STATUS_COMMAND = 0x03 // Phone -> Watch: Request current app UUID
} AppRunStateCommand;
void app_run_state_send_update(const Uuid *uuid, AppState app_state);
// Currently only needed for backwards support of the 0x32 endpoint, at which point
// it will become static and not exported.
void app_run_state_command(CommSession *session, AppRunStateCommand cmd, const Uuid *uuid);

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/app_launch_button.h"
#include "applib/app_launch_reason.h"
#include "services/common/compositor/compositor.h"
typedef struct LaunchConfigCommon {
AppLaunchReason reason;
ButtonId button;
const void *args;
const CompositorTransition *transition;
} LaunchConfigCommon;

View file

@ -0,0 +1,164 @@
/*
* 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 "launcher_app_message.h"
#include "app_run_state.h"
#include "applib/app_message/app_message_internal.h"
#include "process_management/app_install_manager.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/dict.h"
#define LAUNCHER_MESSAGE_ENDPOINT_ID (0x31)
typedef enum {
//! Used as reply from the watch to the phone, to indicate the app is not running.
//! Or, when pushed from phone to watch, this value will have the effect of killing the app.
NOT_RUNNING_DEPRECATED = 0x00,
//! Used as reply from the watch to the phone, to indicate the app is running.
//! Or, when pushed from phone to watch, this value will have the effect of launching the app.
RUNNING_DEPRECATED = 0x01,
} AppStateDeprecated;
enum {
//! This key/value can be pushed from the phone to the watch to launch
//! or kill an app on the watch.
RUN_STATE_KEY = 0x01, // TUPLE_UINT8
STATE_FETCH_KEY = 0x02, // TUPLE_UINT8
};
static uint8_t s_transaction_id;
// For unit testing
void launcher_app_message_reset(void) {
s_transaction_id = 0;
}
void launcher_app_message_send_app_state_deprecated(const Uuid *uuid, bool running) {
// Deprecated: 0x31 endpoint, only used by Android versions < 2.2 and iOS
AppStateDeprecated app_state = running ? RUNNING_DEPRECATED : NOT_RUNNING_DEPRECATED;
uint8_t buffer[sizeof(AppMessagePush) + sizeof(Tuple) + sizeof(uint32_t)];
AppMessagePush *push_message = (AppMessagePush *)buffer;
*push_message = (const AppMessagePush) {
.header = {
.command = CMD_PUSH,
.transaction_id = s_transaction_id++,
},
.uuid = *uuid,
};
uint32_t size = sizeof(Dictionary) + sizeof(Tuple) + sizeof(uint32_t);
const Tuplet tuplet = TupletInteger(RUN_STATE_KEY, (uint32_t) app_state);
PBL_ASSERTN(DICT_OK == dict_serialize_tuplets_to_buffer(
&tuplet, 1, (uint8_t *)&push_message->dictionary, &size));
comm_session_send_data(comm_session_get_system_session(), LAUNCHER_MESSAGE_ENDPOINT_ID,
(const uint8_t *)buffer, sizeof(buffer), COMM_SESSION_DEFAULT_TIMEOUT);
}
static bool prv_has_invalid_length(size_t expected, size_t actual) {
if (actual < expected) {
PBL_LOG(LOG_LEVEL_ERROR, "Too short");
return true;
}
return false;
}
static bool prv_receive_push_cmd(CommSession *session,
AppMessagePush *push_message, size_t length) {
if (prv_has_invalid_length(sizeof(AppMessagePush), length)) {
return false;
}
bool success = false;
// Scan the dictionary:
const size_t dict_size = length - sizeof(AppMessagePush) + sizeof(Dictionary);
DictionaryIterator iter;
const Tuple *tuple = dict_read_begin_from_buffer(&iter,
(const uint8_t *) &push_message->dictionary,
dict_size);
while (tuple) {
uint8_t cmd = tuple->key;
switch (cmd) {
case RUN_STATE_KEY:
if (tuple->value->uint8 != RUNNING_DEPRECATED) {
cmd = APP_RUN_STATE_STOP_COMMAND;
} else {
cmd = APP_RUN_STATE_RUN_COMMAND;
}
success = true;
break;
case STATE_FETCH_KEY:
cmd = APP_RUN_STATE_STATUS_COMMAND;
success = true;
break;
default:
cmd = APP_RUN_STATE_INVALID_COMMAND;
}
// Call into app_run_state to take the action (to avoid duping code):
const Uuid *app_uuid = &push_message->uuid;
app_run_state_command(NULL, (AppRunStateCommand)cmd, app_uuid);
tuple = dict_read_next(&iter);
}
return success;
}
static void prv_send_ack_nack_reply(CommSession *session, const uint8_t transaction_id, bool ack) {
const AppMessageAck nack_message = {
.header = {
.command = ack ? CMD_ACK : CMD_NACK,
.transaction_id = transaction_id,
},
};
comm_session_send_data(session, LAUNCHER_MESSAGE_ENDPOINT_ID,
(const uint8_t *) &nack_message, sizeof(nack_message),
COMM_SESSION_DEFAULT_TIMEOUT);
}
void launcher_app_message_protocol_msg_callback_deprecated(CommSession *session,
const uint8_t* data, size_t length) {
if (prv_has_invalid_length(sizeof(AppMessageHeader), length)) {
return;
}
AppMessageHeader *message = (AppMessageHeader *) data;
bool ack = false;
switch (message->command) {
case CMD_PUSH: {
// Incoming message:
ack = prv_receive_push_cmd(session, (AppMessagePush *) message, length);
break;
}
case CMD_ACK:
case CMD_NACK:
// Ignore ACK / NACKs
return;
default:
// Ignore everything else
break;
}
prv_send_ack_nack_reply(session, message->transaction_id, ack);
}

View file

@ -0,0 +1,26 @@
/*
* 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 "util/uuid.h"
//! Launcher App Message is deprecated and on Android >= 2.3 and other devices that pass the
//! support flags for the AppRunState endpoint will use that endpoint (0x34) instead. That
//! endpoint should be used for sending messages on start/stop status of applications and
//! sending/recieving application states. The LauncherAppMessage endpoint is kept for
//! backwards compability with older mobile applications.
void launcher_app_message_send_app_state_deprecated(const Uuid *uuid, bool running);

View file

@ -0,0 +1,25 @@
/*
* 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 "pebble_process_info.h"
int version_compare(Version a, Version b) {
if (a.major != b.major) {
return a.major - b.major;
}
return a.minor - b.minor;
}

View file

@ -0,0 +1,274 @@
/*
* 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 <stdint.h>
//! @addtogroup Foundation
//! @{
//! @addtogroup App
//! @{
//! Application metadata flags.
//! Flags can be combined using the `|` operator.
//! @see PBL_PROCESS_INFO
typedef enum {
//! Use to indicate the process is a "standard" app.
//! The system will show the app in the main menu.
PROCESS_INFO_STANDARD_APP = 0,
//! Use to indicate the process is a watchface.
//! The system will show the process in the watchfaces menu.
PROCESS_INFO_WATCH_FACE = 1 << 0,
//! Use to hide the process.
PROCESS_INFO_VISIBILITY_HIDDEN = 1 << 1,
//! Use to hide the process, unless there is ongoing communication with
//! the companion smartphone application.
PROCESS_INFO_VISIBILITY_SHOWN_ON_COMMUNICATION = 1 << 2,
//! Use to indicate the process allows Javascript API access
PROCESS_INFO_ALLOW_JS = 1 << 3,
//! Use to indicate the process should have a worker.bin installed as well.
PROCESS_INFO_HAS_WORKER = 1 << 4,
//! True, if process uses RockyJS APIs
PROCESS_INFO_ROCKY_APP = 1 << 5,
//! Bitmask, to store compile time platform
PROCESS_INFO_PLATFORM_MASK = 0xf << 6,
//! SDK older than 4.2 doesn't store any value
PROCESS_INFO_PLATFORM_UNKNOWN = 0x0 << 6,
PROCESS_INFO_PLATFORM_APLITE = 0x1 << 6,
//! Values that are actually added by SDK 4.2+
PROCESS_INFO_PLATFORM_BASALT = 0x2 << 6,
PROCESS_INFO_PLATFORM_CHALK = 0x3 << 6,
PROCESS_INFO_PLATFORM_DIORITE = 0x4 << 6,
PROCESS_INFO_PLATFORM_EMERY = 0x5 << 6,
} PebbleProcessInfoFlags;
//! @} // group App
//! @} // group Foundation
// struct PebbleProcessInfo change log
// ================================
// struct_version (little endian):
// 0x0800 -- sdk_version and process_version uint16_t fields added (Grand Slam / 1.7)
// .major:0x08 .minor:0x01 -- all version fields split up into minor/major; uuid field appended (Junior Whopper / 2.0?)
// .major:0x08 .minor:0x02 -- 2.0, added resource crc and resource timestamp
// .major:0x09 .minor:0x00 -- 2.0, no more reloc_list_start
// .major:0x10 .minor:0x00 -- 2.0, added virtual_size
#define PROCESS_INFO_CURRENT_STRUCT_VERSION_MAJOR 0x10
#define PROCESS_INFO_CURRENT_STRUCT_VERSION_MINOR 0x0
// process info version for last know 1.x
// let this be a warning to engineers everywhere
// who want to design a system with fancy versioning and
// support
#define PROCESS_INFO_LEGACY_STRUCT_VERSION_MAJOR 0x08
// SDK change log
// ================================
// sdk.major:0x4 .minor:0x0 -- Bump the SDK version to make 1.x and 2.x apps distinguishable
// sdk.major:0x5 .minor:0x0 -- Bump the SDK version for breaking AppMessage changes b/t 2.x alpha and beta releases
// sdk.major:0x5 .minor:0x1 -- Added additional API functions (MenuLayer callbacks)
// sdk.major:0x5 .minor:0x2 -- Changed app heap double free behaviour.
// sdk.major:0x5 .minor:0x3 -- Added number_window_get_window (API v2.0 revision 12)
// sdk.major:0x5 .minor:0x4 -- Added gbitmap_create_blank (API v2.0 revision 13) and click_recognizer_is_repeating (rev 14)
// sdk.major:0x5 .minor:0x5 -- Added accel_raw_data_service_subscribe and related types (rev 15)
// sdk.major:0x5 .minor:0x6 -- Added background worker APIs (rev 16)
// sdk.major:0x5 .minor:0x7 -- Added heap_bytes_free / heap_bytes_used (rev 17)
// sdk.major:0x5 .minor:0x8 -- Added compass APIs (rev 18)
// sdk.major:0x5 .minor:0x9 -- Added Uuid utility APIs (rev 19)
// sdk.major:0x5 .minor:0xa -- Fixed gpath behaviour, added gpath_draw_filled_legacy. (rev 20)
// sdk.major:0x5 .minor:0xb -- Added custom animation curves (rev 21)
// sdk.major:0x5 .minor:0xc -- Added API for model, color and firmware version of watch (rev 22)
// sdk.major:0x5 .minor:0xd -- Added direct access to frame buffer (rev 23)
// sdk.major:0x5 .minor:0xe -- Added wakeup API, app_launch_reason (rev 24)
// sdk.major:0x5 .minor:0xf -- Added clock_is_timezone_set in preparation for timezone support (rev 25)
// sdk.major:0x5 .minor:0x10 -- Added the first i18n API: get_locale (rev 26)
// sdk.major:0x5 .minor:0x11 -- Added second i18n API: setlocale (rev 27)
// sdk.major:0x5 .minor:0x13 -- Export mktime (rev 29)
// sdk.major:0x5 .minor:0x14 -- Rev 30 was a move of several color APIs as to not conflict with release 2.9 sdk. Several of these functions were removed after rev 55 for 3.0-beta10
// sdk.major:0x5 .minor:0x15 -- Added timezone APIs (rev 31)
// sdk.major:0x5 .minor:0x16 -- Added 3.0 animation API (rev 32)
// sdk.major:0x5 .minor:0x17 -- Export new gbitmap accessors (rev 33)
// sdk.major:0x5 .minor:0x18 -- Export gbitmap_sequence API (rev 34)
// sdk.major:0x5 .minor:0x19 -- Export gbitmap_create_from_png_data API (rev 35)
// sdk.major:0x5 .minor:0x20 -- Export new complex animations API (rev 36)
// sdk.major:0x5 .minor:0x21 -- Export missing accessors animations APIs (rev 37)
// sdk.major:0x5 .minor:0x22 -- Export launch_get_args() API (rev 38)
// sdk.major:0x5 .minor:0x23 -- Export 3.0 menu_layer_create() and menu_layer_shadow_enable() (rev 39)
// sdk.major:0x5 .minor:0x24 -- Added additional calls to gbitmap_sequence (rev 40)
// sdk.major:0x5 .minor:0x25 -- Export antialiased flag and stroke width in GContext (rev 41)
// sdk.major:0x5 .minor:0x26 -- Added 3.0 ActionBar (rev 42)
// sdk.major:0x5 .minor:0x27 -- Added gbitmap_sequence_update_bitmap_by_elapsed (rev 43)
// sdk.major:0x5 .minor:0x28 -- Export menu_layer_cell_is_highlighted() and gbitmap_create_palettized_from_1bit() (rev 44)
// sdk.major:0x5 .minor:0x29 -- Export graphics_draw_rotated_bitmap() API (rev 45)
// sdk.major:0x5 .minor:0x2a -- Added action_bar_layer_set_icon_press_animation (rev 46)
// sdk.major:0x5 .minor:0x2b -- Created TextLayerLegacy2 (rev 47)
// sdk.major:0x5 .minor:0x2c -- Added GDrawCommand (rev 48)
// sdk.major:0x5 .minor:0x2d -- Added property_animation_update_uint32 (rev 49)
// sdk.major:0x5 .minor:0x2e -- Added gpath_draw_outline_open (rev 50)
// sdk.major:0x5 .minor:0x2f -- Fixed legacy time() support (rev 51)
// sdk.major:0x5 .minor:0x30 -- Deprecate inverter layer (rev 52)
// sdk.major:0x5 .minor:0x31 -- Added default menu layer colors (rev 53)
// sdk.major:0x5 .minor:0x32 -- Added menu_layer_set_callbacks (rev 54)
// sdk.major:0x5 .minor:0x33 -- API cleanup, loop_count --> play_count (rev 55)
// sdk.major:0x5 .minor:0x34 -- Added menu_layer_pad_bottom_enable (rev 56)
// sdk.major:0x5 .minor:0x35 -- Export initial version of StatusBarLayer (rev 57)
// sdk.major:0x5 .minor:0x36 -- Export difftime (rev 58)
// sdk.major:0x5 .minor:0x37 -- Fixed legacy time_ms() support (rev 59)
// sdk.major:0x5 .minor:0x38 -- Export gcolor_legible_over (rev 60)
// sdk.major:0x5 .minor:0x39 -- Export property_animation_update_gcolor8 (rev 61)
// sdk.major:0x5 .minor:0x3a -- Export app_focus_service_subscribe_handlers (rev 62)
// sdk.major:0x5 .minor:0x3b -- Export ActionMenu APIs (rev 63)
// sdk.major:0x5 .minor:0x3c -- Export Smartstrap APIs (rev 64)
// sdk.major:0x5 .minor:0x3d -- Added support for timeline resources in PBWs (rev 65)
// sdk.major:0x5 .minor:0x3e -- Update Connection Service APIs (rev 66)
// sdk.major:0x5 .minor:0x3f -- Enabled support for 8K App Messages (rev 67)
// sdk.major:0x5 .minor:0x40 -- Allow disabling of error dialogs for the voice API (rev 68)
// sdk.major:0x5 .minor:0x41 -- DEG_TO_TRIGANGLE (rev 69)
// sdk.major:0x5 .minor:0x42 -- Export gbitmap_get_data_row_info (rev 70)
// sdk.major:0x5 .minor:0x43 -- Export ContentIndicator (rev 71)
// sdk.major:0x5 .minor:0x45 -- Export grect_inset (rev 73)
// sdk.major:0x5 .minor:0x46 -- Export fill_radial, draw_arc, etc. (rev 74)
// sdk.major:0x5 .minor:0x47 -- Text flow and scroll layer pagination (rev 75)
// sdk.major:0x5 .minor:0x48 -- Export menu_layer_is_index_selected and round menu layer cell heights (rev 76)
// sdk.major:0x5 .minor:0x49 -- Added font v3 (no API changes) (rev 76)
// sdk.major:0x5 .minor:0x4a -- HealthAPI (rev 77)
// sdk.major:0x5 .minor:0x4b -- More time utility functions (rev 78)
// sdk.major:0x5 .minor:0x4c -- More Health service API calls (rev 79)
// sdk.major:0x5 .minor:0x4d -- Add health_service_get_measurement_system_for_display() (rev 80)
// sdk.major:0x5 .minor:0x4e -- Export gdraw_command_frame_get_command_list() (rev 81)
// sdk.major:0x5 .minor:0x4f -- Add new version of gcolor_equal() and deprecate old version (rev 82)
// sdk.major:0x5 .minor:0x50 -- 4.0: Add health service expansion, UnobstructedArea, AppGlance, AppExitReason, result duration (rev 83)
// sdk.major:0x5 .minor:0x51 -- Add memory_cache_flush() (rev 84)
// sdk.major:0x5 .minor:0x52 -- rocky_event_loop_with_resource (rev. 85)
// sdk.major:0x5 .minor:0x53 -- Add health_service heart rate sampling period support (rev 86)
// sdk.major:0x5 .minor:0x54 -- Add PlatformType enum and defines (rev 87)
// sdk.major:0x5 .minor:0x55 -- Preferred Content Size (rev 88)
// sdk.major:0x5 .minor:0x56 -- Add PlatformType enum and defines (rev 89)
#define PROCESS_INFO_CURRENT_SDK_VERSION_MAJOR 0x5
#define PROCESS_INFO_CURRENT_SDK_VERSION_MINOR 0x56
// The first SDK to ship with 2.x APIs
#define PROCESS_INFO_FIRST_2X_SDK_VERSION_MAJOR 0x4
#define PROCESS_INFO_FIRST_2X_SDK_VERSION_MINOR 0x0
// The first SDK to ship with 3.x APIs
#define PROCESS_INFO_FIRST_3X_SDK_VERSION_MAJOR 0x5
#define PROCESS_INFO_FIRST_3X_SDK_VERSION_MINOR 0x16
// The first SDK to ship with 4.x APIs
#define PROCESS_INFO_FIRST_4X_SDK_VERSION_MAJOR 0x5
#define PROCESS_INFO_FIRST_4X_SDK_VERSION_MINOR 0x50
// The first SDK that has a stable Rocky.js contract + stores platforms in binary
#define PROCESS_INFO_FIRST_4_2_X_SDK_VERSION_MAJOR 0x5
#define PROCESS_INFO_FIRST_4_2_X_SDK_VERSION_MINOR 0x54
#define PROCESS_NAME_BYTES 32
#define COMPANY_NAME_BYTES 32
//! @internal
//! Version data structure with minor & major versions: When making non-backwards-compatible changes,
//! the major version should get bumped. When making a change (e.g. to the PebbleProcessInfo struct) that is backwards
//! compatible (e.g. adding a field at the end), you should only bump the minor version.
typedef struct __attribute__((__packed__)) {
uint8_t major; //!< "compatibility" version number
uint8_t minor;
} Version;
//! @return 0 if a and b are equal, >0 if a > b, <0 if b > a
int version_compare(Version a, Version b);
//! @internal
// WARNING: changes in this struct must be reflected in:
// - tintin/waftools/inject_metadata.py
// - iOS/PebblePrivateKit/PebblePrivateKit/PBBundle.m
typedef struct __attribute__((__packed__)) {
char header[8]; //!< Sentinal value, should always be 'PBLAPP'
Version struct_version; //!< version of this structure's format
Version sdk_version; //!< version of the SDK used to build this process
Version process_version; //!< version of the process
uint16_t load_size; //!< size of the binary in flash, including this metadata but not the reloc table
uint32_t offset; //!< The entry point of this executable
uint32_t crc; //!< CRC of the data only, ie, not including this struct or the reloc table at the end
char name[PROCESS_NAME_BYTES]; //!< Name to display on the menu
char company[COMPANY_NAME_BYTES]; //!< Name of the maker of this process
uint32_t icon_resource_id; //!< Resource ID within this bank to use as a 32x32 icon
uint32_t sym_table_addr; //!< The system will poke the sdk's symbol table address into this field on load
uint32_t flags; //!< Bitwise OR of PebbleProcessInfoFlags
uint32_t num_reloc_entries; //!< The number of entries in the address relocation list
struct __attribute__((__packed__)) {
uint8_t byte0;
uint8_t byte1;
uint8_t byte2;
uint8_t byte3;
uint8_t byte4;
uint8_t byte5;
uint8_t byte6;
uint8_t byte7;
uint8_t byte8;
uint8_t byte9;
uint8_t byte10;
uint8_t byte11;
uint8_t byte12;
uint8_t byte13;
uint8_t byte14;
uint8_t byte15;
} uuid; //!< The process's UUID
uint32_t resource_crc; //!< CRC of the resource data only
uint32_t resource_timestamp; //!< timestamp of the resource data
uint16_t virtual_size; //!< The total amount of memory used by the process (.text + .data + .bss)
} PebbleProcessInfo;
//! @internal
typedef struct __attribute__((__packed__)) {
char header[8]; //!< Sentinal value, should always be 'PBLAPP'
Version struct_version; //!< version of this structure's format
Version sdk_version; //!< version of the SDK used to build this process
Version process_version; //!< version of the process
uint16_t load_size; //!< size of the binary in flash, including this metadata but not the reloc table
uint32_t offset; //!< The entry point of this executable
uint32_t crc; //!< CRC of the data only, ie, not including this struct or the reloc table at the end
char name[PROCESS_NAME_BYTES]; //!< Name to display on the menu
char company[COMPANY_NAME_BYTES]; //!< Name of the maker of this process
uint32_t icon_resource_id; //!< Resource ID within this process's bank to use as a 32x32 icon
uint32_t sym_table_addr; //!< The system will poke the sdk's symbol table address into this field on load
uint32_t flags; //!< Bitwise OR of PebbleProcessInfoFlags
uint32_t reloc_list_start; //!< The offset of the address relocation list
uint32_t num_reloc_entries; //!< The number of entries in the address relocation list
struct __attribute__((__packed__)) {
uint8_t byte0;
uint8_t byte1;
uint8_t byte2;
uint8_t byte3;
uint8_t byte4;
uint8_t byte5;
uint8_t byte6;
uint8_t byte7;
uint8_t byte8;
uint8_t byte9;
uint8_t byte10;
uint8_t byte11;
uint8_t byte12;
uint8_t byte13;
uint8_t byte14;
uint8_t byte15;
} uuid; //!< The process's UUID
} LegacyPebbleAppInfo;

View file

@ -0,0 +1,293 @@
/*
* 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 "pebble_process_md.h"
#include "process_loader.h"
#include "board/display.h"
#include <string.h>
//////////////////////
// Md Field Accessors
//////////////////////
const char* process_metadata_get_name(const PebbleProcessMd *md) {
if (md->process_storage == ProcessStorageFlash) {
return ((const PebbleProcessMdFlash*) md)->name;
} else if (md->process_storage == ProcessStorageResource) {
return ((const PebbleProcessMdResource*) md)->name;
}
return ((const PebbleProcessMdSystem*) md)->name;
}
uint32_t process_metadata_get_size_bytes(const PebbleProcessMd *md) {
if (md->process_storage == ProcessStorageFlash) {
return ((const PebbleProcessMdFlash*) md)->size_bytes;
} else if (md->process_storage == ProcessStorageResource) {
return ((const PebbleProcessMdResource*) md)->size_bytes;
}
return 0;
}
Version process_metadata_get_process_version(const PebbleProcessMd *md) {
if (md->process_storage == ProcessStorageFlash) {
return ((const PebbleProcessMdFlash*) md)->process_version;
}
return (Version) { 0, 0 };
}
Version process_metadata_get_sdk_version(const PebbleProcessMd *md) {
if (md->process_storage == ProcessStorageFlash) {
return ((const PebbleProcessMdFlash*) md)->sdk_version;
}
return (Version) { PROCESS_INFO_CURRENT_SDK_VERSION_MAJOR, PROCESS_INFO_CURRENT_SDK_VERSION_MINOR };
}
ProcessAppRunLevel process_metadata_get_run_level(const PebbleProcessMd *md) {
if (md->process_storage == ProcessStorageFlash) {
return ProcessAppRunLevelNormal;
} else if (md->process_storage == ProcessStorageResource) {
return ProcessAppRunLevelNormal;
}
return ((const PebbleProcessMdSystem*) md)->run_level;
}
int process_metadata_get_code_bank_num(const PebbleProcessMd *md) {
if (md->process_storage == ProcessStorageFlash) {
return ((const PebbleProcessMdFlash*) md)->code_bank_num;
}
return 0;
}
int process_metadata_get_res_bank_num(const PebbleProcessMd *md) {
if (md->process_storage == ProcessStorageFlash) {
return ((const PebbleProcessMdFlash*) md)->res_bank_num;
}
return 0;
}
ResourceVersion process_metadata_get_res_version(const PebbleProcessMd *md) {
if (md->process_storage == ProcessStorageFlash) {
return ((const PebbleProcessMdFlash*) md)->res_version;
}
return (ResourceVersion) { 0, 0 } ;
}
const uint8_t *process_metadata_get_build_id(const PebbleProcessMd *md) {
if (md->process_storage == ProcessStorageFlash) {
return ((const PebbleProcessMdFlash*) md)->build_id;
}
return NULL;
}
//////////////////////
// Md Builders
//////////////////////
static void prv_init_from_info_common(PebbleProcessMd *common, const PebbleProcessInfo *info,
PebbleTask task, ProcessStorage process_storage) {
memcpy(&common->uuid, &info->uuid, sizeof(Uuid));
common->process_storage = process_storage;
// Flags
common->process_type = process_metadata_flags_process_type(info->flags, task);
common->visibility = process_metadata_flags_visibility(info->flags);
common->allow_js = process_metadata_flags_allow_js(info->flags);
common->has_worker = process_metadata_flags_has_worker(info->flags);
common->is_rocky_app = process_metadata_flags_rocky_app(info->flags);
common->stored_sdk_platform = process_metadata_flags_stored_sdk_platform(info->flags);
common->is_unprivileged = true;
// We don't know the load address of the process until the process is
// actually loaded, so we can't convert the entry point's offset into
// an address until it's actually been loaded into that address.
// Just shove the unmodified offset into the struct and let the
// process loader convert it into an absolute address.
// TODO: refactor the PebbleProcessMd stuff so that metadata that
// isn't needed before load or can't be fully resolved until then
// is left out of PebbleProcessMdCommon.
common->main_func = (void *)(uintptr_t)info->offset;
}
void process_metadata_init_with_flash_header(PebbleProcessMdFlash *md,
const PebbleProcessInfo *info, int code_bank_num, PebbleTask task, uint8_t *build_id_buffer) {
*md = (PebbleProcessMdFlash){};
prv_init_from_info_common(&md->common, info, task, ProcessStorageFlash);
// Flash app specific fields
strncpy(md->name, info->name, sizeof(md->name));
md->name[sizeof(md->name) - 1] = 0;
md->size_bytes = info->virtual_size;
md->process_version = info->process_version;
md->sdk_version = info->sdk_version;
md->code_bank_num = code_bank_num;
md->res_bank_num = code_bank_num;
md->res_version = (ResourceVersion) {
.crc = info->resource_crc,
.timestamp = info->resource_timestamp
};
if (build_id_buffer) {
memcpy(md->build_id, build_id_buffer, BUILD_ID_EXPECTED_LEN);
}
}
void process_metadata_init_with_resource_header(PebbleProcessMdResource *md,
const PebbleProcessInfo *info, int bin_resource_id, PebbleTask task) {
*md = (PebbleProcessMdResource){};
prv_init_from_info_common(&md->common, info, task, ProcessStorageResource);
// Resource app specific fields
strncpy(md->name, info->name, sizeof(md->name));
md->name[sizeof(md->name) - 1] = 0;
md->size_bytes = info->virtual_size;
md->bin_resource_id = bin_resource_id;
}
//////////////////////////////////
// PebbleProcessInfoFlags Helpers
//////////////////////////////////
ProcessVisibility process_metadata_flags_visibility(PebbleProcessInfoFlags flags) {
if (flags & PROCESS_INFO_VISIBILITY_HIDDEN) {
return ProcessVisibilityHidden;
} else if (flags & PROCESS_INFO_VISIBILITY_SHOWN_ON_COMMUNICATION) {
return ProcessVisibilityShownOnCommunication;
} else {
return ProcessVisibilityShown;
}
}
ProcessType process_metadata_flags_process_type(PebbleProcessInfoFlags flags, PebbleTask task) {
// Flags
if (flags & PROCESS_INFO_WATCH_FACE) {
return ProcessTypeWatchface;
} else if (task == PebbleTask_Worker) {
// BG_TODO: Set a bit in the PebbleProcessInfo to indicate it's a worker instead of having to
// pass in the is_worker argument. Need to update process_metadata_get_flags_bitfield() to
// match as well.
return ProcessTypeWorker;
} else {
return ProcessTypeApp;
}
}
bool process_metadata_flags_allow_js(PebbleProcessInfoFlags flags) {
return ((flags & PROCESS_INFO_ALLOW_JS) != 0);
}
bool process_metadata_flags_has_worker(PebbleProcessInfoFlags flags) {
return ((flags & PROCESS_INFO_HAS_WORKER) != 0);
}
bool process_metadata_flags_rocky_app(PebbleProcessInfoFlags flags) {
return ((flags & PROCESS_INFO_ROCKY_APP) != 0);
}
uint16_t process_metadata_flags_stored_sdk_platform(PebbleProcessInfoFlags flags) {
return (flags & PROCESS_INFO_PLATFORM_MASK);
}
static const Version first_3x_version = {
PROCESS_INFO_FIRST_3X_SDK_VERSION_MAJOR,
PROCESS_INFO_FIRST_3X_SDK_VERSION_MINOR,
};
static const Version first_4x_version = {
PROCESS_INFO_FIRST_4X_SDK_VERSION_MAJOR,
PROCESS_INFO_FIRST_4X_SDK_VERSION_MINOR,
};
static const Version first_4_2_version = {
PROCESS_INFO_FIRST_4_2_X_SDK_VERSION_MAJOR,
PROCESS_INFO_FIRST_4_2_X_SDK_VERSION_MINOR,
};
PlatformType process_metadata_get_app_sdk_platform(const PebbleProcessMd *md) {
if (!md->is_unprivileged) {
return PBL_PLATFORM_TYPE_CURRENT;
}
const Version app_sdk_version = process_metadata_get_sdk_version(md);
// 2.0 <= SDK < 3.0
if (version_compare(app_sdk_version, first_3x_version) < 0) {
// 2.x SDKs didn't support anything but Aplite
return PlatformTypeAplite;
}
// 3.0 <= SDK < 4.0
if (version_compare(app_sdk_version, first_4x_version) < 0) {
return PBL_PLATFORM_SWITCH(PBL_PLATFORM_TYPE_CURRENT,
/* aplite */ PlatformTypeAplite, // unreachable, since we don't build for Tintin anymore
/* basalt */ PlatformTypeBasalt,
/* chalk */ PlatformTypeChalk,
/* diorite */ PlatformTypeAplite, // there's was no Diorite SDK prior to 4.0
/* emery */ PlatformTypeBasalt);
}
// 4.0 <= SDK < 4.2
if (version_compare(app_sdk_version, first_4_2_version) < 0) {
return PBL_PLATFORM_SWITCH(PBL_PLATFORM_TYPE_CURRENT,
/* aplite */ PlatformTypeAplite, // unreachable, since we don't build for Tintin anymore
/* basalt */ PlatformTypeBasalt,
/* chalk */ PlatformTypeChalk,
/* diorite */ PlatformTypeDiorite, // there's was no Aplite SDK after 4.0
/* emery */ PlatformTypeBasalt);
}
// 4.2 <= SDK --> the flags should be filled correctly.
const uint32_t stored_value = md->stored_sdk_platform;
switch (stored_value) {
case PROCESS_INFO_PLATFORM_APLITE:
return PlatformTypeAplite;
case PROCESS_INFO_PLATFORM_BASALT:
return PlatformTypeBasalt;
case PROCESS_INFO_PLATFORM_CHALK:
return PlatformTypeChalk;
case PROCESS_INFO_PLATFORM_DIORITE:
return PlatformTypeDiorite;
case PROCESS_INFO_PLATFORM_EMERY:
return PlatformTypeEmery;
default: {
// If we encounter an unknown platform, we assume that it's meant for the current platform
// (as it's most-likely a system-app). This is not a security risk as developers could always
// patch the binaries as they wish anyway
return PBL_PLATFORM_TYPE_CURRENT;
}
}
}
ProcessAppSDKType process_metadata_get_app_sdk_type(const PebbleProcessMd *md) {
if (!md->is_unprivileged) {
return ProcessAppSDKType_System;
} else {
const Version app_sdk_version = process_metadata_get_sdk_version(md);
if (version_compare(app_sdk_version, first_3x_version) < 0) {
return ProcessAppSDKType_Legacy2x;
} else if (version_compare(app_sdk_version, first_4x_version) < 0) {
return ProcessAppSDKType_Legacy3x;
} else {
return ProcessAppSDKType_4x;
}
}
}

View file

@ -0,0 +1,193 @@
/*
* 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/platform.h"
#include "kernel/pebble_tasks.h"
#include "resource/resource.h"
#include "util/build_id.h"
#include "pebble_process_info.h"
#include "util/uuid.h"
#include <stdbool.h>
typedef void (*PebbleMain)(void);
typedef enum {
ProcessVisibilityShown = 0,
ProcessVisibilityHidden = 1,
ProcessVisibilityShownOnCommunication = 2,
ProcessVisibilityQuickLaunch = 3,
} ProcessVisibility;
typedef enum {
ProcessTypeApp = 0,
ProcessTypeWatchface = 1,
ProcessTypeWorker = 2,
} ProcessType;
typedef enum ProcessAppRunLevel {
ProcessAppRunLevelNormal = 0,
ProcessAppRunLevelSystem = 1,
ProcessAppRunLevelCritical = 2
} ProcessAppRunLevel;
typedef enum {
ProcessStorageBuiltin = 0,
ProcessStorageFlash = 1,
ProcessStorageResource = 2
} ProcessStorage;
typedef enum {
ProcessAppSDKType_System,
ProcessAppSDKType_Legacy2x,
ProcessAppSDKType_Legacy3x,
ProcessAppSDKType_4x
} ProcessAppSDKType;
//! This structure is used internally to describe the process. This struct here is actually a polymorphic base
//! class, and can be casted to either \ref PebbleProcessMdSystem or \ref PebbleProcessMdFlash depending on the value
//! of \ref is_flash_based. Clients shouldn't do this casting themselves though, and instead should use the
//! process_metadata_get_* functions to safely retreive values from this struct.
typedef struct PebbleProcessMd {
Uuid uuid;
//! The address of the main function of the process. This will be inside the firmware for firmware processes and
//! will be inside the process's RAM region for 3rd party processes.
PebbleMain main_func;
//! The type of process
ProcessType process_type;
// Flags
ProcessVisibility visibility;
//! Where is the process stored?
ProcessStorage process_storage;
//! Can this process call kernel functionality directly or does it need to go through syscalls?
bool is_unprivileged;
//! Allow Javascript applications to access this process
bool allow_js;
//! This process has a sister worker process in flash.
bool has_worker;
//! Process is allowed to call RockyJS APIs
bool is_rocky_app;
//! Bits of the sdk_platform as they were stored in the binary, or 0 if undefined
uint16_t stored_sdk_platform;
} PebbleProcessMd;
//! App metadata for apps that are built into the firmware.
typedef struct PebbleProcessMdSystem {
PebbleProcessMd common;
const char* name;
uint32_t icon_resource_id;
//! The level at which the process runs. Any processes that try to start but they have a lower level than what's
//! set using the \ref app_manager_set_minimum_run_level() function will not be launched.
ProcessAppRunLevel run_level;
} PebbleProcessMdSystem;
//! Metadata for processes that are dynamically loaded from flash.
typedef struct PebbleProcessMdFlash {
PebbleProcessMd common;
char name[PROCESS_NAME_BYTES];
//! Size in bytes of the app region that is occupied when this app is loaded
//! Used when sizing the app heap. For first-party apps, this value will
//! always be zero.
uint16_t size_bytes;
//! The version specified by the author for this process
Version process_version;
//! The version of the SDK this process was created with.
Version sdk_version;
//! The bank this process will get it's code and data from. This field is only valid if the
//! \ref process_storage is ProcessStorageFlash
uint32_t code_bank_num;
//! The bank this app will get its resources from
ResAppNum res_bank_num;
//! A version we can use to verify the resources in the resource bank on the filesystem are valid
ResourceVersion res_version;
//! Build id of the application
uint8_t build_id[BUILD_ID_EXPECTED_LEN];
} PebbleProcessMdFlash;
//! Metadata for processes that are dynamically loaded from a system resource.
typedef struct {
PebbleProcessMd common;
char name[PROCESS_NAME_BYTES];
//! Size in bytes of the app region that is occupied when this app is loaded
//! Used when sizing the app heap.
uint16_t size_bytes;
//! The resource number of the app binary
uint32_t bin_resource_id;
} PebbleProcessMdResource;
const char* process_metadata_get_name(const PebbleProcessMd *md);
uint32_t process_metadata_get_size_bytes(const PebbleProcessMd *md);
Version process_metadata_get_process_version(const PebbleProcessMd *md);
Version process_metadata_get_sdk_version(const PebbleProcessMd *md);
ProcessAppRunLevel process_metadata_get_run_level(const PebbleProcessMd *md);
int process_metadata_get_code_bank_num(const PebbleProcessMd *md);
int process_metadata_get_res_bank_num(const PebbleProcessMd *md);
ResourceVersion process_metadata_get_res_version(const PebbleProcessMd *md);
const uint8_t *process_metadata_get_build_id(const PebbleProcessMd *md);
//! @param[out] md
void process_metadata_init_with_flash_header(PebbleProcessMdFlash *md,
const PebbleProcessInfo *flash_header, int process_bank_num, PebbleTask task,
uint8_t *build_id_buffer);
//! @param[out] md
void process_metadata_init_with_resource_header(PebbleProcessMdResource *md,
const PebbleProcessInfo *info, int bin_resource_id, PebbleTask task);
ProcessVisibility process_metadata_flags_visibility(PebbleProcessInfoFlags flags);
ProcessType process_metadata_flags_process_type(PebbleProcessInfoFlags flags, PebbleTask task);
bool process_metadata_flags_allow_js(PebbleProcessInfoFlags flags);
bool process_metadata_flags_has_worker(PebbleProcessInfoFlags flags);
bool process_metadata_flags_rocky_app(PebbleProcessInfoFlags flags);
uint16_t process_metadata_flags_stored_sdk_platform(PebbleProcessInfoFlags flags);
ProcessAppSDKType process_metadata_get_app_sdk_type(const PebbleProcessMd *md);
PlatformType process_metadata_get_app_sdk_platform(const PebbleProcessMd *md);

View file

@ -0,0 +1,91 @@
/*
* 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 "process_heap.h"
#include "applib/app_logging.h"
#include "process_management/pebble_process_info.h"
#include "process_management/pebble_process_md.h"
#include "system/passert.h"
#include <util/heap.h>
static void prv_warn_on_double_free(void *ptr) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Double free detected on pointer <%p>", ptr);
}
static void prv_croak_on_double_free(void *ptr) {
// Always log regardless of CROAKing on unprivileged apps. We don't send the
// croak message out over APP_LOG correctly so if we didn't do this developers
// wouldn't see the croak reason.
APP_LOG(APP_LOG_LEVEL_ERROR, "Double free detected on pointer <%p>", ptr);
PBL_CROAK("Double free detected on pointer <%p>", ptr);
}
static void prv_warn_on_heap_corruption(void *ptr) {
// They're using 3.2 SDK or older, just let them off with a log message.
APP_LOG(APP_LOG_LEVEL_ERROR, "Error: Heap corrupt around <%p>", ptr);
}
static void prv_croak_on_heap_corruption(void *ptr) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Error: Heap corrupt around <%p>", ptr);
PBL_CROAK("Error: Heap corrupt around <%p>", ptr);
}
void process_heap_set_exception_handlers(Heap *heap,
const PebbleProcessMd *app_md) {
// prior to version 2.1 of the firmware (app sdk version 5.2), we never had
// double free detection in our heap and we would just silently ignore someone
// trying to free an invalid pointer. going forward we want to let our
// developers know that this happened as firmly as possible. if an app is
// compiled with the old sdk, yell at them through a log message so we don't
// break any existing apps. if the app is compiled with a new sdk after we
// made this change, just crash their app.
static const Version old_style_double_free_handling_version = { 5, 1 };
const Version app_sdk_version = process_metadata_get_sdk_version(app_md);
if (version_compare(app_sdk_version,
old_style_double_free_handling_version) <= 0) {
heap_set_double_free_handler(heap, prv_warn_on_double_free);
} else {
heap_set_double_free_handler(heap, prv_croak_on_double_free);
}
// We try to detect heap corruption by looking at segment headers and
// comparing the sizes of and prevSizes of consecutive blocks.
//
// This isn't bulletproof, but it's better than nothing. It's possible that
// corruption is happening that doesn't affect the block headers (eg. use of a
// dangling pointer), or that the overflow data simply matches what we wanted
// to check anyways.
//
// There is no risk of this producing false positives, as any header
// inconsistency is invalid.
//
// For some strange reason, some apps seem to be able to withstand heap
// corruption. An example of this is overwriting the prevSize heap member with
// a 0. An app will survive fine like this as long as we don't need to
// traverse the heap in reverse.
//
// Since some apps can continue to run without issue, rather than tearing
// everything down and create a bad user experience, let's hope that
// developers read the logs and fix their apps.
static const Version old_style_heap_corruption_version = { 5, 0x38 };
if (version_compare(app_sdk_version, old_style_heap_corruption_version) < 0) {
// They're using 3.2 SDK or older, just let them off with a log message.
heap_set_corruption_handler(heap, prv_warn_on_heap_corruption);
} else {
heap_set_corruption_handler(heap, prv_croak_on_heap_corruption);
}
}

View file

@ -0,0 +1,25 @@
/*
* 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
typedef struct Heap Heap;
typedef struct PebbleProcessMd PebbleProcessMd;
//! Configure exception handlers (corruption and double-free) for
//! app and worker heaps.
void process_heap_set_exception_handlers(Heap *heap,
const PebbleProcessMd *app_md);

View file

@ -0,0 +1,40 @@
/*
* 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 "kernel/pebble_tasks.h"
#include <stdint.h>
#include <stdbool.h>
typedef struct MemorySegment MemorySegment;
typedef struct PebbleProcessMd PebbleProcessMd;
//! Load the process image specified by app_md into memory.
//!
//! The memory that the process image is loaded into is split from the
//! destination memory segment. The destination memory segment must
//! already be zeroed out.
//!
//! Only the process' text, data and bss are loaded and split from the
//! memory segment. It is the caller's responsibility to set up the
//! process stack and heap.
//!
//! @return pointer to process's entry point function, or NULL if the
//! process loading failed.
void * process_loader_load(const PebbleProcessMd *app_md, PebbleTask task,
MemorySegment *destination);

View file

@ -0,0 +1,799 @@
/*
* 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 "process_manager.h"
#include "app_install_manager.h"
#include "app_manager.h"
#include "worker_manager.h"
#include "applib/app_logging.h"
#include "applib/accel_service_private.h"
#include "applib/platform.h"
#include "applib/rockyjs/rocky_res.h"
#include "applib/ui/dialogs/dialog.h"
#include "applib/ui/dialogs/expandable_dialog.h"
#include "process_state/app_state/app_state.h"
#include "process_state/worker_state/worker_state.h"
#include "pebble_process_md.h"
#include "kernel/pebble_tasks.h"
#include "os/tick.h"
#include "resource/resource_ids.auto.h"
#include "services/common/animation_service.h"
#include "services/common/analytics/analytics.h"
#include "services/common/evented_timer.h"
#include "services/common/event_service.h"
#include "services/common/hrm/hrm_manager.h"
#include "services/normal/filesystem/pfs.h"
#include "services/common/system_task.h"
#include "services/normal/accessory/smartstrap_attribute.h"
#include "services/normal/app_cache.h"
#include "services/normal/data_logging/data_logging_service.h"
#include "services/normal/persist.h"
#include "services/normal/voice/voice.h"
#include "shell/normal/watchface.h"
#include "syscall/syscall.h"
#include "system/logging.h"
#include "system/passert.h"
#include "kernel/pbl_malloc.h"
#include "kernel/ui/modals/modal_manager.h"
#include "util/heap.h"
#include "syscall/syscall_internal.h"
#include "apps/system_apps/app_fetch_ui.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
static TimerID s_deinit_timer_id = TIMER_INVALID_ID;
// -------------------------------------------------------------------------------------------
static ProcessContext *prv_get_context_for_task(PebbleTask task) {
if (task == PebbleTask_App) {
return app_manager_get_task_context();
} else {
PBL_ASSERTN(task == PebbleTask_Worker);
return worker_manager_get_task_context();
}
}
// -------------------------------------------------------------------------------------------
static ProcessContext *prv_get_context(void) {
return prv_get_context_for_task(pebble_task_get_current());
}
// --------------------------------------------------------------------------------------------------
// This timer callback gets called if the process doesn't finish it's deinit within the required timeout (currently
// 3 seconds).
static void prv_graceful_close_timer_callback(void* data) {
PBL_LOG(LOG_LEVEL_DEBUG, "deinit timeout expired, killing app forcefully");
PebbleTask task = (PebbleTask)data;
process_manager_put_kill_process_event(task, false /*gracefully*/);
}
// ---------------------------------------------------------------------------------------------
static bool prv_force_stop_task_if_unprivileged(ProcessContext *context) {
vTaskSuspend((TaskHandle_t) context->task_handle);
uint32_t control_reg = ulTaskDebugGetStackedControl((TaskHandle_t) context->task_handle);
if ((control_reg & 0x1) == 0) {
// We're priviledged, it's not safe to just kill the app task.
vTaskResume((TaskHandle_t) context->task_handle);
return false;
}
context->safe_to_kill = true;
return true;
}
// --------------------------------------------------------------------------------------------------
static void prv_force_close_timer_callback(void* data) {
PebbleTask task = (PebbleTask)data;
ProcessContext *context = prv_get_context_for_task(task);
if (!prv_force_stop_task_if_unprivileged(context)) {
PBL_CROAK("task stuck inside privileged code!");
}
process_manager_put_kill_process_event(task, false /*graceful*/);
}
// ---------------------------------------------------------------------------------------------
EXTERNALLY_VISIBLE void process_manager_handle_syscall_exit(void) {
PebbleTask task = pebble_task_get_current();
ProcessContext *context = prv_get_context_for_task(task);
if (context->closing_state == ProcessRunState_ForceClosing) {
PBL_LOG(LOG_LEVEL_DEBUG, "Hit syscall exit trap!");
context->safe_to_kill = true;
process_manager_put_kill_process_event(task, false);
vTaskSuspend(xTaskGetCurrentTaskHandle());
}
}
// ---------------------------------------------------------------------------------------------
void process_manager_init(void) {
s_deinit_timer_id = new_timer_create();
}
// -----------------------------------------------------------------------------------------------------------
void process_manager_put_kill_process_event(PebbleTask task, bool gracefully) {
PebbleEvent event = {
.type = PEBBLE_PROCESS_KILL_EVENT,
.kill = {
.gracefully = gracefully,
.task = task,
},
};
// When we have decided to exit the app,
// it doesn't need to process any queued accel data
// or other services before exiting, so clear the to_process_event_queue
ProcessContext *context = prv_get_context_for_task(task);
if (context->to_process_event_queue) {
event_queue_cleanup_and_reset(context->to_process_event_queue);
}
// Since the app is about to exit, make sure the next (only)
// message on the from app queue is the PEBBLE_APP_KILL_EVENT
// to expedite exiting the application
event_reset_from_process_queue(task);
event_put_from_process(task, &event);
}
// ---------------------------------------------------------------------------------------------
//! Init the context variables for a task.
void process_manager_init_context(ProcessContext* context,
const PebbleProcessMd *app_md, const void *args) {
PBL_ASSERT_TASK(PebbleTask_KernelMain);
PBL_ASSERTN(context->task_handle == NULL);
PBL_ASSERTN(context->to_process_event_queue == NULL);
context->app_md = app_md;
AppInstallId install_id = app_install_get_id_for_uuid(&app_md->uuid);
context->install_id = install_id;
// we are safe to kill until the app main starts
context->safe_to_kill = true;
context->closing_state = ProcessRunState_Running;
context->args = args;
context->user_data = 0;
// get app launch reason and wakeup_info
context->launch_reason = app_manager_get_launch_reason();
context->launch_button = app_manager_get_launch_button();
context->wakeup_info = app_manager_get_app_wakeup_state();
// set the default exit reason
context->exit_reason = APP_EXIT_NOT_SPECIFIED;
}
#if !defined(RECOVERY_FW)
bool process_manager_check_SDK_compatible(const AppInstallId id) {
AppInstallEntry entry;
if (!app_install_get_entry_for_install_id(id, &entry)) {
return false;
}
if (app_install_entry_is_SDK_compatible(&entry)) {
return true;
}
PBL_LOG(LOG_LEVEL_WARNING, "App requires support for SDK version (%"PRIu8".%"PRIu8"), "
"we only support version (%"PRIu8".%"PRIu8").",
entry.sdk_version.major, entry.sdk_version.minor,
(uint8_t) PROCESS_INFO_CURRENT_SDK_VERSION_MAJOR,
(uint8_t) PROCESS_INFO_CURRENT_SDK_VERSION_MINOR);
ExpandableDialog *expandable_dialog = expandable_dialog_create("Incompatible SDK");
Dialog *dialog = expandable_dialog_get_dialog(expandable_dialog);
const char *error_text = i18n_noop("This app requires a newer version of the Pebble firmware.");
dialog_set_text(dialog, i18n_get(error_text, expandable_dialog));
dialog_set_icon(dialog, RESOURCE_ID_GENERIC_WARNING_SMALL);
i18n_free(error_text, expandable_dialog);
if (pebble_task_get_current() == PebbleTask_KernelMain) {
WindowStack *window_stack = modal_manager_get_window_stack(ModalPriorityAlert);
expandable_dialog_push(expandable_dialog, window_stack);
} else {
app_expandable_dialog_push(expandable_dialog);
}
return false;
}
static bool prv_needs_fetch(AppInstallId id, const PebbleProcessMd **md, bool is_worker) {
PBL_ASSERTN(md);
if (!app_cache_entry_exists(id)) {
PBL_LOG(LOG_LEVEL_DEBUG, "Cache entry did not exist on launch attempt");
return true;
}
*md = app_install_get_md(id, is_worker);
if (!is_worker && rocky_app_validate_resources(*md) == RockyResourceValidation_Invalid) {
PBL_LOG(LOG_LEVEL_DEBUG, "App has incompatible JavaScript bytecode");
// TODO: do we need to purge the app cache here?
return true;
}
return false;
}
#endif
void process_manager_launch_process(const ProcessLaunchConfig *config) {
PBL_ASSERT_TASK(PebbleTask_KernelMain);
const AppInstallId id = config->id;
const bool is_worker = config->worker;
if (id == INSTALL_ID_INVALID) {
PBL_LOG(LOG_LEVEL_DEBUG, "Invalid ID");
return;
}
const PebbleProcessMd *md = NULL;
#if !RECOVERY_FW
if (app_install_id_from_app_db(id)) {
if (!process_manager_check_SDK_compatible(id)) {
return;
}
// This is a third party flash 3.0 app install
if (prv_needs_fetch(id, &md, is_worker)) {
PBL_LOG(LOG_LEVEL_DEBUG, "Cache entry did not exist on launch attempt");
// Freed in app_fetch_ui.c
AppFetchUIArgs *fetch_args = kernel_malloc_check(sizeof(AppFetchUIArgs));
*fetch_args = (AppFetchUIArgs){};
fetch_args->common = config->common;
fetch_args->app_id = id;
fetch_args->forcefully = config->forcefully;
// if the data is wakeup info, then copy out that information.
if ((config->common.reason == APP_LAUNCH_WAKEUP) && (config->common.args != NULL)) {
fetch_args->wakeup_info = *(WakeupInfo *)config->common.args;
fetch_args->common.args = &fetch_args->wakeup_info;
}
PebbleEvent e = {
.type = PEBBLE_APP_FETCH_REQUEST_EVENT,
.app_fetch_request = {
.id = id,
.with_ui = true,
.fetch_args = fetch_args,
},
};
event_put(&e);
return;
} else {
// tell the app cache that we are launching this application.
app_cache_app_launched(id);
}
}
#endif
// we either came here if PRF or if we didn't start a fetch
// md is either already initialized, or we took a code path that didn't try
if (!md) {
md = app_install_get_md(id, is_worker);
}
if (!md) {
PBL_LOG(LOG_LEVEL_ERROR, "Tried to launch non-existant app!");
return;
}
#if !RECOVERY_FW
if (!is_worker) {
// Check if the app ram size is valid in order to determine if its SDK version is supported.
if (!app_manager_is_app_supported(md)) {
PBL_LOG(LOG_LEVEL_WARNING, "Tried to launch an app with an unsupported SDK version.");
AppInstallEntry entry;
if (!app_install_get_entry_for_install_id(id, &entry)) {
// can't retrieve app install entry for id
PBL_LOG(LOG_LEVEL_ERROR, "Failed to get entry for id %"PRId32, id);
} else if (app_install_entry_is_watchface(&entry)) {
// If the watchface is for an unsupported SDK version, we need to switch the default
// watchface back to tictoc. Otherwise, we will be stuck in the launcher forever.
watchface_set_default_install_id(INSTALL_ID_INVALID);
watchface_launch_default(NULL);
}
// Not going to launch this, release the allocated memory
app_install_release_md(md);
return;
}
}
#endif
if (is_worker) {
worker_manager_launch_new_worker_with_args(md, NULL);
} else {
app_manager_launch_new_app(&(AppLaunchConfig) {
.md = md,
.common = config->common,
.forcefully = config->forcefully,
});
}
}
// ---------------------------------------------------------------------------------------------
extern void analytics_external_collect_app_cpu_stats(void);
extern void analytics_external_collect_app_flash_read_stats(void);
static void prv_handle_app_stop_analytics(const ProcessContext *const context,
PebbleTask task, bool gracefully) {
if (!gracefully) {
if (task == PebbleTask_App) {
if (context->app_md->is_rocky_app) {
analytics_inc(ANALYTICS_APP_METRIC_ROCKY_CRASHED_COUNT, AnalyticsClient_App);
}
analytics_inc(ANALYTICS_APP_METRIC_CRASHED_COUNT, AnalyticsClient_App);
} else if (task == PebbleTask_Worker) {
analytics_inc(ANALYTICS_APP_METRIC_BG_CRASHED_COUNT, AnalyticsClient_Worker);
}
if (context->app_md->is_rocky_app) {
analytics_inc(ANALYTICS_DEVICE_METRIC_APP_ROCKY_CRASHED_COUNT, AnalyticsClient_System);
}
analytics_inc(ANALYTICS_DEVICE_METRIC_APP_CRASHED_COUNT, AnalyticsClient_System);
}
if (task == PebbleTask_App) {
analytics_stopwatch_stop(ANALYTICS_APP_METRIC_FRONT_MOST_TIME);
}
analytics_external_collect_app_cpu_stats();
analytics_external_collect_app_flash_read_stats();
}
// ---------------------------------------------------------------------------------------------
//! This method returns true if the process is safe to kill (it has exited out of it's main function). If the
//! the process is not already safe to kill, it will "prod" it to exit, set a timer, and return false.
//!
//! The app manager and worker manager MUST call this before they call the code to kill the task and clean it up
//! (most of that work is done by process_manager_process_cleanup()). If it returns false, they should abort the
//! current process exit operation and wait for another KILL event to get posted.
//!
//! If the task does eventually fall through it's main function, the exit handling code will set the safe to kill
//! boolean and post another KILL event to the KernelMain which will result in this method being called again, and
//! this time it will see the safe to kill is set and return true
//!
//! If the task does not exit by itself before the timer expires, then the timer will post another KILL event
//! with graceful set to false. This will result in this method being alled again with gracefully = false. When
//! we see this, we just try and make sure the app is not stuck in privilege code. If it's not, we return true
//! and allow the caller to kill the task.
//!
//! If however, the task is in privilege mode, we tell the syscall machinery to set the safe to kill boolean as
//! soon as the current syscall returns and set another timer. Once that timer expires, if the task is no longer
//! in privilege mode we post another KILL event (graceful = false). If the task is still in privilege mode then,
//! we croak.
bool process_manager_make_process_safe_to_kill(PebbleTask task, bool gracefully) {
PBL_ASSERT_TASK(PebbleTask_KernelMain);
ProcessContext *context = prv_get_context_for_task(task);
// If already safe to kill, we're done
if (context->safe_to_kill) {
prv_handle_app_stop_analytics(context, task, gracefully);
return true;
}
PBL_LOG(LOG_LEVEL_DEBUG, "make %s process safe to kill: state %u", pebble_task_get_name(task),
context->closing_state);
if (gracefully) {
if (context->closing_state == ProcessRunState_Running) {
context->closing_state = ProcessRunState_GracefullyClosing;
PBL_LOG(LOG_LEVEL_DEBUG, "Attempting to gracefully deinit %s", pebble_task_get_name(task));
// Send deinit event to app:
PebbleEvent deinit_event = {
.type = PEBBLE_PROCESS_DEINIT_EVENT,
};
process_manager_send_event_to_process(task, &deinit_event);
// Set a timer to forcefully close the app in 3 seconds if it doesn't respond by then. The app can respond
// within 3 seconds by posting a PEBBLE_APP_KILL_EVENT (graceful=true), which will result in
// app_manager_close_current_app() being called, which in turn calls this method with graceful = true.
bool success = new_timer_start(s_deinit_timer_id, 3 * 1000, prv_graceful_close_timer_callback, (void*)task,
0 /*flags*/);
PBL_ASSERTN(success);
}
// Else we're already in the gracefully closing state, just let the timer run out or the
// app to mark itself as safe_to_kill.
} else {
PBL_LOG(LOG_LEVEL_DEBUG, "Check if we can force stop the %s task", pebble_task_get_name(task));
if (prv_force_stop_task_if_unprivileged(context)) {
PBL_LOG(LOG_LEVEL_DEBUG, "Got it");
prv_handle_app_stop_analytics(context, task, gracefully);
return true;
}
// Non-graceful close
if (context->closing_state == ProcessRunState_Running ||
context->closing_state == ProcessRunState_GracefullyClosing) {
// Right before a syscall drops privilege, it calls
// process_manager_force_close_syscall_exit_trap to check whether
// it is about to return control to a misbehaving app. This
// function checks the process context's closing state and makes
// the process safe to kill if its state is set to ForceClosing.
// All we have to do is set the state and wait.
context->closing_state = ProcessRunState_ForceClosing;
PBL_LOG(LOG_LEVEL_DEBUG, "task is privileged, setting the syscall exit trap");
bool success = new_timer_start(s_deinit_timer_id, 3 * 1000, prv_force_close_timer_callback, (void*)task,
0 /*flags*/);
PBL_ASSERTN(success);
}
}
return false;
}
// -----------------------------------------------------------------------------------------------------------
// This is designed to be called from the task itself, in privilege mode, after it exits. It is called from
// app_task_exit for app tasks and worker_task_exit from worker tasks
NORETURN process_manager_task_exit(void) {
PebbleTask task = pebble_task_get_current();
ProcessContext *context = prv_get_context_for_task(task);
// If this is not a system app, output its heap usage stats.
if (context->app_md->process_storage == ProcessStorageFlash) {
const Heap *heap;
if (task == PebbleTask_App) {
heap = app_state_get_heap();
} else if (task == PebbleTask_Worker) {
heap = worker_state_get_heap();
} else {
WTF;
}
// FIXME: We cast heap_size's size_t result to int because for some reason our printf doesn't
// like the %zd formatter
APP_LOG(APP_LOG_LEVEL_INFO, "Heap Usage for %s: Total Size <%dB> Used <%uB> Still allocated <%uB>",
pebble_task_get_name(task), (int) heap_size(heap), heap->high_water_mark, heap->current_size);
}
// Let the task manager know we're done cleaning up.
context->safe_to_kill = true;
// Tell the task manager that we want to be killed. This may be redundant if we're responding to a DEINIT
// message, but just in case we're exiting on our own (someone found the sys_exit syscall and called in when
// we weren't expecting it?) we should let the app manager know.
process_manager_put_kill_process_event(task, true);
// Better to die in our sleep ...
vTaskSuspend(NULL /* self */);
// We don't expect someone to resume us.
PBL_CROAK("");
}
// ---------------------------------------------------------------------------------------------
// Get the args for the current process
const void *process_manager_get_current_process_args(void) {
return prv_get_context()->args;
}
// ---------------------------------------------------------------------------------------------
// Setup the system services required for this process. Called by app_manager and worker_manager
// right before we launch the task for the new process.
void process_manager_process_setup(PebbleTask task) {
ProcessContext *context = prv_get_context_for_task(task);
persist_service_client_open(&context->app_md->uuid);
}
// ---------------------------------------------------------------------------------------------
//! Kills the process, giving it no chance to clean things up or exit gracefully. The proces must already be in a
//! state where it's safe to exit, so the caller must call process_manager_make_process_safe_to_kill() first and only
//! call this method if process_manager_make_process_safe_to_kill() returns true;
void process_manager_process_cleanup(PebbleTask task) {
PBL_ASSERT_TASK(PebbleTask_KernelMain);
ProcessContext *context = prv_get_context_for_task(task);
PBL_ASSERTN(context->safe_to_kill);
PBL_LOG(LOG_LEVEL_DEBUG, "%s is getting cleaned up", pebble_task_get_name(task));
// Shutdown services that may be running. Do this before we destory the task and clear the queue
// just in case other services are still in flight.
accel_service_cleanup_task_session(task);
animation_service_cleanup(task);
persist_service_client_close(&context->app_md->uuid);
event_reset_from_process_queue(task);
evented_timer_clear_process_timers(task);
event_service_clear_process_subscriptions(task);
#if CAPABILITY_HAS_BUILTIN_HRM
hrm_manager_process_cleanup(task, context->install_id);
#endif
#ifndef RECOVERY_FW
#if CAPABILITY_HAS_MICROPHONE
voice_kill_app_session(task);
#endif
dls_inactivate_sessions(task);
if (task == PebbleTask_App) {
#if CAPABILITY_HAS_ACCESSORY_CONNECTOR
smartstrap_attribute_unregister_all();
#endif
}
#endif // RECOVERY_FW
// Unregister the task
pebble_task_unregister(task);
new_timer_stop(s_deinit_timer_id);
if (context->task_handle) {
vTaskDelete(context->task_handle);
context->task_handle = NULL;
}
// cleanup memory that was used to store the Md, but only if it isn't a system application
app_install_release_md(context->app_md);
// Clear the old app metadata
context->app_md = 0;
context->install_id = INSTALL_ID_INVALID;
if (context->to_process_event_queue &&
pdFAIL == event_queue_cleanup_and_reset(context->to_process_event_queue)) {
PBL_LOG(LOG_LEVEL_ERROR, "The to processs queue could not be reset!");
}
context->to_process_event_queue = NULL;
}
// -----------------------------------------------------------------------------------------------------------
void process_manager_close_process(PebbleTask task, bool gracefully) {
if (task == PebbleTask_App) {
// This will tell the app manager to switch to the last registered app.
app_manager_close_current_app(gracefully);
} else if (task == PebbleTask_Worker) {
worker_manager_close_current_worker(gracefully);
} else {
WTF;
}
}
// ----------------------------------------------------------------------------------------------
bool process_manager_send_event_to_process(PebbleTask task, PebbleEvent* e) {
ProcessContext *context = prv_get_context_for_task(task);
if (context->to_process_event_queue == 0) {
PBL_LOG(LOG_LEVEL_WARNING, "Dropped app event! Type: %u", e->type);
return false;
}
// Put on app's own queue:
if (!xQueueSend(context->to_process_event_queue, e, milliseconds_to_ticks(1000))) {
PBL_LOG(LOG_LEVEL_ERROR, "Failed to send event %u to app! Closing it!", e->type);
// We could be called from a timer task callback, so post a kill event rather than call
// process_manager_close_process directly.
process_manager_put_kill_process_event(task, false);
return false;
}
return true;
}
// ----------------------------------------------------------------------------------------------
uint32_t process_manager_process_events_waiting(PebbleTask task) {
ProcessContext *context = prv_get_context_for_task(task);
if (context->to_process_event_queue == 0) {
PBL_LOG(LOG_LEVEL_WARNING, "no event queue");
return 0;
}
return uxQueueMessagesWaiting(context->to_process_event_queue);
}
// ----------------------------------------------------------------------------------------------
void process_manager_send_callback_event_to_process(PebbleTask task, void (*callback)(void *data), void *data) {
PBL_ASSERTN(callback != NULL);
PebbleEvent event = {
.type = PEBBLE_CALLBACK_EVENT,
.callback = {
.callback = callback,
.data = data,
},
};
process_manager_send_event_to_process(task, &event);
}
// ----------------------------------------------------------------------------------------------
void *process_manager_address_to_offset(PebbleTask task, void *system_address) {
ProcessContext *context = prv_get_context_for_task(task);
if (system_address >= context->load_start &&
system_address < context->load_end) {
return (void*)((uintptr_t) system_address - (uintptr_t)context->load_start);
}
// Not in app space:
return system_address;
}
// ----------------------------------------------------------------------------------------------
extern char __APP_RAM__[];
extern char __APP_RAM_end__[];
extern char __WORKER_RAM__[];
extern char __WORKER_RAM_end__[];
bool process_manager_is_address_in_region(PebbleTask task, const void *address,
const void *lower_bound) {
void *ram_start = NULL, *ram_end = NULL;
if (task == PebbleTask_App) {
ram_start = __APP_RAM__;
ram_end = __APP_RAM_end__;
} else if (task == PebbleTask_Worker) {
ram_start = __WORKER_RAM__;
ram_end = __WORKER_RAM_end__;
} else {
WTF;
}
// check for vulnerability: lower_bound outside of task's region
PBL_ASSERTN(lower_bound >= ram_start);
return (address >= lower_bound && address < ram_end);
}
// -------------------------------------------------------------------------------------------
DEFINE_SYSCALL(void, sys_get_pebble_event, PebbleEvent *event) {
if (PRIVILEGE_WAS_ELEVATED) {
syscall_assert_userspace_buffer(event, sizeof(*event));
}
xQueueReceive(prv_get_context()->to_process_event_queue, event, portMAX_DELAY);
}
// -------------------------------------------------------------------------------------------
DEFINE_SYSCALL(AppLaunchReason, sys_process_get_launch_reason, void) {
return prv_get_context()->launch_reason;
}
// -------------------------------------------------------------------------------------------
DEFINE_SYSCALL(ButtonId, sys_process_get_launch_button, void) {
return prv_get_context()->launch_button;
}
// -------------------------------------------------------------------------------------------
DEFINE_SYSCALL(uint32_t, sys_process_get_launch_args, void) {
if (sys_process_get_launch_reason() != APP_LAUNCH_TIMELINE_ACTION) {
return 0;
} else {
return (uint32_t) process_manager_get_current_process_args();
}
}
// -------------------------------------------------------------------------------------------
DEFINE_SYSCALL(AppExitReason, sys_process_get_exit_reason, void) {
return prv_get_context()->exit_reason;
}
// -------------------------------------------------------------------------------------------
DEFINE_SYSCALL(void, sys_process_set_exit_reason, AppExitReason exit_reason) {
// Just return if exit_reason is invalid
if (exit_reason >= NUM_EXIT_REASONS) {
return;
}
prv_get_context()->exit_reason = exit_reason;
}
// -------------------------------------------------------------------------------------------
DEFINE_SYSCALL(void, sys_process_get_wakeup_info, WakeupInfo *info) {
if (PRIVILEGE_WAS_ELEVATED) {
syscall_assert_userspace_buffer(info, sizeof(*info));
}
*info = prv_get_context()->wakeup_info;
}
// -------------------------------------------------------------------------------------------
DEFINE_SYSCALL(const PebbleProcessMd*, sys_process_manager_get_current_process_md, void) {
return prv_get_context()->app_md;
}
// -------------------------------------------------------------------------------------------
DEFINE_SYSCALL(bool, sys_process_manager_get_current_process_uuid, Uuid *uuid_out) {
if (PRIVILEGE_WAS_ELEVATED) {
syscall_assert_userspace_buffer(uuid_out, sizeof(*uuid_out));
}
const PebbleProcessMd* app_md = prv_get_context()->app_md;
if (!app_md) {
return false;
}
*uuid_out = app_md->uuid;
return true;
}
// -------------------------------------------------------------------------------------------
DEFINE_SYSCALL(AppInstallId, sys_process_manager_get_current_process_id, void) {
return prv_get_context()->install_id;
}
// -------------------------------------------------------------------------------------------
DEFINE_SYSCALL(bool, process_manager_compiled_with_legacy2_sdk, void) {
PebbleTask task = pebble_task_get_current();
if (task != PebbleTask_App && task != PebbleTask_Worker) {
return false;
}
const ProcessAppSDKType sdk_type =
process_metadata_get_app_sdk_type(sys_process_manager_get_current_process_md());
return sdk_type == ProcessAppSDKType_Legacy2x;
}
// -------------------------------------------------------------------------------------------
DEFINE_SYSCALL(bool, process_manager_compiled_with_legacy3_sdk, void) {
PebbleTask task = pebble_task_get_current();
if (task != PebbleTask_App && task != PebbleTask_Worker) {
return false;
}
const ProcessAppSDKType sdk_type =
process_metadata_get_app_sdk_type(sys_process_manager_get_current_process_md());
return sdk_type == ProcessAppSDKType_Legacy3x;
}
DEFINE_SYSCALL(Version, sys_get_current_process_sdk_version, void) {
return process_metadata_get_sdk_version(sys_process_manager_get_current_process_md());
}
DEFINE_SYSCALL(PlatformType, process_manager_current_platform, void) {
PebbleTask task = pebble_task_get_current();
if (task != PebbleTask_App && task != PebbleTask_Worker) {
return PBL_PLATFORM_TYPE_CURRENT;
}
const PebbleProcessMd *const md = sys_process_manager_get_current_process_md();
return process_metadata_get_app_sdk_platform(md);
}

View file

@ -0,0 +1,183 @@
/*
* 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 "launch_config.h"
#include "applib/app_exit_reason.h"
#include "applib/platform.h"
#include "kernel/events.h"
#include "process_management/app_install_types.h"
#include "process_management/pebble_process_md.h"
#include "services/common/accel_manager.h"
#include "services/common/compositor/compositor.h"
#include <stdbool.h>
// Used to identify invalid or system app when using app_bank calls
#define INVALID_BANK_ID ~0
#define SYSTEM_APP_BANK_ID (~0 - 1)
typedef enum ProcessRunState {
ProcessRunState_Running,
ProcessRunState_GracefullyClosing,
ProcessRunState_ForceClosing
} ProcessRunState;
typedef struct ProcessContext {
//! The app metadata structure if this process represents a running application (NULL otherwise). Describes the
//! static information about the currently running app.
const PebbleProcessMd* app_md;
//! The app install id for this process if it represents an application.
AppInstallId install_id;
//! The FreeRTOS task we're using the run the app. See xTaskHandle
void* task_handle;
//! The address range the process was loaded into. It is used to
//! convert physical addresses into relative addresses in order to
//! assist developers when debugging their apps.
void *load_start;
void *load_end;
//! Queue used to send events to the process. The process will read PebbleEvents from here using
//! sys_get_pebble_event.
void* to_process_event_queue;
//! This bool indicates that we can safely stop and delete the process without causing
//! any instability to the rest of the system. This is set in both graceful (process closing
//! and returning) and non-graceful (force closes or crashes) cases. The task itself
//! is the only task that's allowed to set this to true.
volatile bool safe_to_kill;
//! Used to provide the application the method used to launch the application
AppLaunchReason launch_reason;
//! The button information that launched the app used to provide the app this information
ButtonId launch_button;
//! Used to allow the application to specify the reason it exited
AppExitReason exit_reason;
//! Used to provide the application the wakeup_event that launched the application
WakeupInfo wakeup_info;
//! What state the process is currently running in. Managed by the app_manager/worker_manager
ProcessRunState closing_state;
//! Arguments passed to the process'. This is a pointer to a struct
//! that is defined by the application.
const void* args;
//! Pointer to a piece of data that we hold on behalf of the process. This makes it so
//! our first party apps don't have to keep declaring their own statics to hold a pointer
//! to a struct to represent their app data. Third party apps don't have this issue as their
//! globals are loaded and unloaded when they start and stop, where the globals for our first
//! party apps are always present. See app_state_get_user_data/app_state_set_user_data
void* user_data;
} ProcessContext;
typedef struct ProcessLaunchConfig {
LaunchConfigCommon common;
AppInstallId id;
//! true if we're launching the worker for this app ID, false if we're launching the app.
bool worker;
//! true if the previous app should be closed forcefully, false otherwise.
bool forcefully;
} ProcessLaunchConfig;
//! Init the process manager
void process_manager_init(void);
//! Init the context variables
void process_manager_init_context(ProcessContext* context,
const PebbleProcessMd *app_md, const void *args);
//! Checks if the app refered to by the given AppInstallId is SDK compatible
//! Returns true if it is compatible, false otherwise
//! Shows an error dialog if it is not compatible
bool process_manager_check_SDK_compatible(const AppInstallId id);
//! Request that a process be launched.
//! @param id The app to launch. Note that this app may not be cached
//! @param args The args to the app
//! @param animation The animation that should be used to show the app
//! @prama launch_reason The launch reason for the app starting
void process_manager_launch_process(const ProcessLaunchConfig *config);
//! Close the given process. This is the highest level call which transfers control to the manager of that type of
//! task. That manager will in turn call process_manager_make_process_safe_to_kill/process_manager_process_cleanup
//! If it's an app, this will cause the next app in the system app state machine to automatically get launched.
//! May only be called from the KernelMain task
void process_manager_close_process(PebbleTask task, bool gracefully);
//! Called from the task itself, as it exits and reenters privileged mode
NORETURN process_manager_task_exit(void);
//! Prod the given process into exiting. Returns true if it is safe to kill that task and clean it up using
//! process_manager_process_cleanup
bool process_manager_make_process_safe_to_kill(PebbleTask task, bool gracefully);
//! Setup some required state for processes
void process_manager_process_setup(PebbleTask task);
//! Kills the task and cleans it up. Must only be called if process_manager_make_process_safe_to_kill() returns
//! true.
void process_manager_process_cleanup(PebbleTask task);
//! Get the args for the current process
const void* process_manager_get_current_process_args(void);
//! Send an event to the process' event loop
bool process_manager_send_event_to_process(PebbleTask task, PebbleEvent* e);
//! Get the number of messages currently waiting to be processed by the process
uint32_t process_manager_process_events_waiting(PebbleTask task);
//! Wrapper for \ref send_event_to_app to send callback events to the app
void process_manager_send_callback_event_to_process(PebbleTask task, void (*callback)(void *), void *data);
//! Send a kill event for the given process to KernelMain
void process_manager_put_kill_process_event(PebbleTask task, bool gracefully);
//! Right before a syscall function drops privilege, it calls this
//! function to check whether the kernel is trying to kill the process.
//! It offers us a place that we know is unprivileged for us to safely
//! suspend the task and shut it down if it is misbehaving.
void process_manager_handle_syscall_exit(void);
//! Return true if the current process was compiled with any 2.x SDK
bool process_manager_compiled_with_legacy2_sdk(void);
//! Return true if the current process was compiled with any 3.x SDK
bool process_manager_compiled_with_legacy3_sdk(void);
//! Returns platform the application was compiled against
//! Use this to test, e.g. if app is Aplite while being executed on Basalt
//! or Basalt while being executed on Emery
PlatformType process_manager_current_platform(void);
//! Check if address is between lower_bound and the end of task's memory region.
//! lower_bound is used by syscall_assert_userspace_buffer to ensure that
//! address doesn't point into the stack frame of the executing syscall
bool process_manager_is_address_in_region(PebbleTask task, const void *address,
const void *lower_bound);
void *process_manager_address_to_offset(PebbleTask task, void *system_address);

View file

@ -0,0 +1,33 @@
/*
* 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.
*/
//! Memory limits for SDK applications
//!
//! These macros describe the memory limits imposed on SDK applications
//! based on the major SDK version they were compiled against. The
//! limits vary depending on both the SDK version and the platform.
//!
//! The APP_RAM_nX_SIZE macros describe the total amount of memory that
//! an SDK app can use directly for stack, heap, text, data and bss.
#pragma once
#include <stddef.h>
#define APP_RAM_SYSTEM_SIZE ((size_t)@APP_RAM_SYSTEM_SIZE@)
#define APP_RAM_2X_SIZE ((size_t)@APP_RAM_2X_SIZE@)
#define APP_RAM_3X_SIZE ((size_t)@APP_RAM_3X_SIZE@)
#define APP_RAM_4X_SIZE ((size_t)@APP_RAM_4X_SIZE@)

View file

@ -0,0 +1,22 @@
/*
* 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 "process_state/app_state/app_state.h"
GContext* app_get_current_graphics_context(void) {
return app_state_get_graphics_context();
}

View file

@ -0,0 +1,35 @@
/*
* 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.
*/
struct GContext;
typedef struct GContext GContext;
//! @addtogroup Foundation
//! @{
//! @addtogroup App
//! @{
//! Gets the graphics context that belongs to the caller of the function.
//! @note Only use the returned GContext inside a drawing callback (`update_proc`)!
//! @return The current graphics context
//! @see \ref Drawing
//! @see \ref LayerUpdateProc
GContext* app_get_current_graphics_context(void);
//! @} // end addtogroup App
//! @} // end addtogroup Foundation

View file

@ -0,0 +1,22 @@
/*
* 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 "sdk_version.h"
bool sdk_version_is_app_messaging_supported(const Version * const sdk_version) {
return ((sdk_version->major == 3 && sdk_version->minor >= 1) ||
sdk_version->major > 3);
}

View file

@ -0,0 +1,24 @@
/*
* 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 "pebble_process_info.h"
#include <stdbool.h>
//! Inspects the app metatdata whether the app supports app messaging.
//! Only if this returns true, the .messaging_info field of PebbleAppHandlers can be used.
//! @return true if the app is built with an SDK that supports app messaging or not
bool sdk_version_is_app_messaging_supported(const Version * const sdk_version);

View file

@ -0,0 +1,420 @@
/*
* 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 "process_manager.h"
#include "worker_manager.h"
#include "process_loader.h"
// Pebble stuff
#include "kernel/event_loop.h"
#include "kernel/pbl_malloc.h"
#include "kernel/util/segment.h"
#include "kernel/util/task_init.h"
#include "mcu/cache.h"
#include "mcu/privilege.h"
#include "os/tick.h"
#include "popups/crashed_ui.h"
#include "process_management/app_install_manager.h"
#include "process_management/app_manager.h"
#include "process_management/process_heap.h"
#include "process_state/worker_state/worker_state.h"
#include "shell/prefs.h"
#include "syscall/syscall.h"
#include "syscall/syscall_internal.h"
#include "system/logging.h"
#include "system/passert.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>
static const int MAX_TO_WORKER_EVENTS = 8;
static ProcessContext s_worker_task_context;
static QueueHandle_t s_to_worker_event_queue;
extern char __WORKER_RAM__[];
extern char __WORKER_RAM_end__[];
extern char __stack_guard_size__[];
//! Used by the "pebble gdb" command to locate the loaded worker in memory.
void * volatile g_worker_load_address;
typedef struct NextWorker {
const PebbleProcessMd *md;
const void *args;
} NextWorker;
static NextWorker s_next_worker;
static bool s_workers_enabled = true;
static AppInstallId s_last_worker_crashed_install_id;
static time_t s_last_worker_crash_timestamp;
static bool s_worker_crash_relaunches_disabled;
// ---------------------------------------------------------------------------------------------
void worker_manager_init(void) {
s_to_worker_event_queue = xQueueCreate(MAX_TO_WORKER_EVENTS, sizeof(PebbleEvent));
}
// ---------------------------------------------------------------------------------------------
// This is the wrapper function for the worker. It's not allowed to return as it's the top frame on the stack
// created for the application.
static void prv_worker_task_main(void *entry_point) {
// Init worker state variables
worker_state_init();
task_init();
// about to start the worker in earnest. No longer safe to kill.
s_worker_task_context.safe_to_kill = false;
// Enter unprivileged mode!
const bool is_unprivileged = s_worker_task_context.app_md->is_unprivileged;
if (is_unprivileged) {
mcu_state_set_thread_privilege(false);
}
const PebbleMain main_func = entry_point;
main_func();
// Clean up after the worker. Remember to put only non-critical cleanup here,
// as the worker may crash or otherwise misbehave. If something really needs to
// be cleaned up, make it so the kernel can do it on the worker's behalf and put
// the call at the bottom of prv_worker_cleanup.
worker_state_deinit();
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_Worker);
}
static size_t prv_get_worker_segment_size(const PebbleProcessMd *app_md) {
// 12 KiB - 640 bytes workerlib static = 11648 bytes
return 11648;
}
static size_t prv_get_worker_stack_size(const PebbleProcessMd *app_md) {
return 1400;
}
// ------------------------------------------------------------------------------------------------
bool worker_manager_launch_new_worker_with_args(const PebbleProcessMd *app_md, const void *args) {
PBL_ASSERT_TASK(PebbleTask_KernelMain);
// Don't launch workers in recovery mode to reduce the chance of crashes
#ifdef RECOVERY_FW
return false;
#endif
// If workers are disabled, don't launch
if (!s_workers_enabled) {
PBL_LOG(LOG_LEVEL_WARNING, "Workers disabled");
return false;
}
// if we are trying to start another worker, then we want to enable relaunches on crashes.
s_worker_crash_relaunches_disabled = false;
// If there is a different worker currently running, tell it to quit first. When it sees s_next_worker
// set, it will call us again once it finishes closing the current worker
if (s_worker_task_context.app_md != NULL && s_worker_task_context.app_md != app_md) {
s_next_worker = (NextWorker) {
.md = app_md,
.args = args,
};
worker_manager_close_current_worker(true /*graceful*/);
return true;
}
// Clear the next worker settings
s_next_worker = (NextWorker) {};
// Error if a worker already launched
if (pebble_task_get_handle_for_task(PebbleTask_Worker) != NULL) {
PBL_LOG(LOG_LEVEL_WARNING, "Worker already launched");
return false;
}
process_manager_init_context(&s_worker_task_context, app_md, args);
s_worker_task_context.to_process_event_queue = s_to_worker_event_queue;
// Set up the worker's memory and load the binary into it.
const size_t worker_segment_size = prv_get_worker_segment_size(app_md);
// The stack guard is counted as part of the app segment size...
const size_t stack_guard_size = (uintptr_t)__stack_guard_size__;
// ...and is carved out of the stack.
const size_t stack_size =
prv_get_worker_stack_size(app_md) - stack_guard_size;
MemorySegment worker_ram = { __WORKER_RAM__, __WORKER_RAM_end__ };
memset((char *)worker_ram.start + stack_guard_size, 0,
memory_segment_get_size(&worker_ram) - stack_guard_size);
MemorySegment worker_segment;
PBL_ASSERTN(memory_segment_split(&worker_ram, &worker_segment,
worker_segment_size));
PBL_ASSERTN(memory_segment_split(&worker_segment, NULL, stack_guard_size));
// No (accessible) memory segments can be placed between the top of WORKER_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 WORKER_RAM; violating this assumption will result in syscalls
// sometimes failing when the worker hasn't done anything wrong.
portSTACK_TYPE *stack = memory_segment_split(&worker_segment, NULL,
stack_size);
PBL_ASSERTN(stack);
s_worker_task_context.load_start = worker_segment.start;
g_worker_load_address = worker_segment.start;
void *entry_point = process_loader_load(app_md, PebbleTask_Worker,
&worker_segment);
s_worker_task_context.load_end = worker_segment.start;
if (!entry_point) {
PBL_LOG(LOG_LEVEL_WARNING, "Tried to launch an invalid worker in bank %u!",
process_metadata_get_code_bank_num(app_md));
return false;
}
// The rest of worker_ram is available for worker state to use as it sees fit.
if (!worker_state_configure(&worker_ram)) {
PBL_LOG(LOG_LEVEL_ERROR, "Worker state configuration failed");
return false;
}
// The remaining space in worker_segment is assigned to the worker's
// heap. Worker state needs to be configured before initializing the
// heap as the WorkerState struct holds the worker heap's Heap object.
Heap *worker_heap = worker_state_get_heap();
PBL_LOG(LOG_LEVEL_DEBUG, "Worker heap init %p %p",
worker_segment.start, worker_segment.end);
heap_init(worker_heap, worker_segment.start, worker_segment.end,
/* enable_heap_fuzzing */ false);
heap_set_lock_impl(worker_heap, (HeapLockImpl) {
.lock_function = prv_heap_lock,
});
process_heap_set_exception_handlers(worker_heap, app_md);
// Init services required for this process before it starts to execute
process_manager_process_setup(PebbleTask_Worker);
char task_name[configMAX_TASK_NAME_LEN];
snprintf(task_name, sizeof(task_name), "Worker <%s>", process_metadata_get_name(s_worker_task_context.app_md));
TaskParameters_t task_params = {
.pvTaskCode = prv_worker_task_main,
.pcName = task_name,
.usStackDepth = stack_size / sizeof(portSTACK_TYPE),
.pvParameters = entry_point,
.uxPriority = (tskIDLE_PRIORITY + 1) | portPRIVILEGE_BIT,
.puxStackBuffer = stack,
};
PBL_LOG(LOG_LEVEL_DEBUG, "Starting %s", task_name);
pebble_task_create(PebbleTask_Worker, &task_params, &s_worker_task_context.task_handle);
// If no default yet, set as the default so that it can be relaunched upon system reset
if (worker_manager_get_default_install_id() == INSTALL_ID_INVALID) {
worker_manager_set_default_install_id(s_worker_task_context.install_id);
}
return true;
}
// ------------------------------------------------------------------------------------------------
// Reset the data we're tracking for workers that crash
static void prv_reset_last_worker_crashed_data(void) {
// No need to reset s_last_worker_crash_timestamp because we always check the install id before
// we check the timestamp
s_last_worker_crashed_install_id = INSTALL_ID_INVALID;
}
// ------------------------------------------------------------------------------------------------
// Launch the next worker, if there is one
void worker_manager_launch_next_worker(AppInstallId previous_worker_install_id) {
// Is there another worker set to switch to?
if (s_next_worker.md != NULL) {
worker_manager_launch_new_worker_with_args(s_next_worker.md, s_next_worker.args);
} else {
// Do we have a default worker we should switch to that is different from the previous worker?
AppInstallId default_id = worker_manager_get_default_install_id();
if (default_id != INSTALL_ID_INVALID && default_id != previous_worker_install_id) {
worker_manager_put_launch_worker_event(default_id);
}
}
}
// ------------------------------------------------------------------------------------------------
void worker_manager_handle_remove_current_worker(void) {
s_worker_crash_relaunches_disabled = true;
worker_manager_close_current_worker(true);
}
// ------------------------------------------------------------------------------------------------
void worker_manager_close_current_worker(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.
// Shouldn't be called from app. Use process_manager_put_kill_process_event() instead.
PBL_ASSERT_TASK(PebbleTask_KernelMain);
// If no worker running, nothing to do
if (!s_worker_task_context.app_md) {
return;
}
// 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_Worker, gracefully)) {
// Maybe next time...
PBL_LOG(LOG_LEVEL_DEBUG, "Worker not ready to exit");
return;
}
// Save which worker we are exiting
AppInstallId closing_worker_install_id = s_worker_task_context.install_id;
// Perform generic process cleanup
process_manager_process_cleanup(PebbleTask_Worker);
// Notify the app install manager that we finally exited
app_install_notify_worker_closed();
// If the worker was closed gracefully, launch any next/default worker and return
if (gracefully) {
// Reset the data tracking the last worker that crashed since the closing worker did not crash
prv_reset_last_worker_crashed_data();
worker_manager_launch_next_worker(closing_worker_install_id);
return;
}
// We arrive here if the worker crashed...
// If the worker's app is in the foreground, close it
if (closing_worker_install_id == app_manager_get_current_app_id()) {
app_manager_force_quit_to_launcher();
} else {
const time_t current_time = rtc_get_time();
const time_t WORKER_CRASH_RESET_TIMEOUT_SECONDS = 60;
if ((closing_worker_install_id == s_last_worker_crashed_install_id) &&
((current_time - s_last_worker_crash_timestamp) <= WORKER_CRASH_RESET_TIMEOUT_SECONDS)) {
// Reset the data tracking the last worker that crashed since we are going to show crash UI
prv_reset_last_worker_crashed_data();
// Show the crash UI, which will ask the user if they want to launch the worker's app
crashed_ui_show_worker_crash(closing_worker_install_id);
} else {
// Record that this worker crashed and what time it crashed
s_last_worker_crashed_install_id = closing_worker_install_id;
s_last_worker_crash_timestamp = current_time;
// Silently restart the worker if we are allowing relaunches of crashed workers
if (!s_worker_crash_relaunches_disabled) {
worker_manager_put_launch_worker_event(closing_worker_install_id);
}
}
}
}
// ------------------------------------------------------------------------------------------------
const PebbleProcessMd* worker_manager_get_current_worker_md(void) {
return s_worker_task_context.app_md;
}
// ------------------------------------------------------------------------------------------------
AppInstallId worker_manager_get_current_worker_id(void) {
return s_worker_task_context.install_id;
}
// ------------------------------------------------------------------------------------------------
ProcessContext* worker_manager_get_task_context(void) {
return &s_worker_task_context;
}
// ------------------------------------------------------------------------------------------------
void worker_manager_put_launch_worker_event(AppInstallId id) {
PBL_ASSERTN(id != INSTALL_ID_INVALID);
PebbleEvent e = {
.type = PEBBLE_WORKER_LAUNCH_EVENT,
.launch_app = {
.id = id,
},
};
event_put(&e);
}
// ------------------------------------------------------------------------------------------------
AppInstallId worker_manager_get_default_install_id(void) {
return worker_preferences_get_default_worker();
}
// ------------------------------------------------------------------------------------------------
void worker_manager_set_default_install_id(AppInstallId id) {
worker_preferences_set_default_worker(id);
}
// ------------------------------------------------------------------------------------------------
void worker_manager_enable(void) {
if (!s_workers_enabled) {
s_workers_enabled = true;
AppInstallId id = worker_manager_get_default_install_id();
if (id != INSTALL_ID_INVALID) {
worker_manager_put_launch_worker_event(id);
}
}
}
// ------------------------------------------------------------------------------------------------
void worker_manager_disable(void) {
if (s_workers_enabled) {
s_workers_enabled = false;
process_manager_put_kill_process_event(PebbleTask_Worker, true /*graceful*/);
}
}
// ------------------------------------------------------------------------------------------------
void command_worker_kill(void) {
process_manager_put_kill_process_event(PebbleTask_Worker, true /*graceful*/);
}
// ------------------------------------------------------------------------------------------------
DEFINE_SYSCALL(AppInstallId, sys_worker_manager_get_current_worker_id, void) {
return worker_manager_get_current_worker_id();
}

View file

@ -0,0 +1,58 @@
/*
* 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 "pebble_process_md.h"
#include "process_manager.h"
#include <stdbool.h>
// Worker management functions
void worker_manager_init(void);
bool worker_manager_launch_new_worker_with_args(const PebbleProcessMd *app_md, const void *args);
void worker_manager_launch_next_worker(AppInstallId previous_worker_install_id);
//! Close the worker and never try to open it again until it is asked to.
void worker_manager_handle_remove_current_worker(void);
void worker_manager_close_current_worker(bool gracefully);
const PebbleProcessMd* worker_manager_get_current_worker_md(void);
AppInstallId worker_manager_get_current_worker_id(void);
ProcessContext* worker_manager_get_task_context(void);
//! Exit the worker. Do some cleanup to make sure things close nicely.
//! Called from the worker task
NORETURN worker_task_exit(void);
void worker_manager_put_launch_worker_event(AppInstallId id);
//! Get the AppInstallId of the default worker to launch. Returns INSTALL_ID_INVALID if none set.
AppInstallId worker_manager_get_default_install_id(void);
void worker_manager_set_default_install_id(AppInstallId id);
//! Can be called to enable worker support. Used by the watch-only mode for example
//! to re-enable workers once the battery level recovers
void worker_manager_enable(void);
//! Can be called to disable worker support. Used by the watch-only mode for example
//! to disable workers when the battery is low. The current worker (if any) will be shut down.
void worker_manager_disable(void);