mirror of
https://github.com/google/pebble.git
synced 2025-11-23 07:50:55 -05:00
894 lines
30 KiB
C
894 lines
30 KiB
C
/*
|
|
* 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(®_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 *) ®_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();
|
|
}
|