mirror of
https://github.com/google/pebble.git
synced 2025-05-12 12:23:16 -04:00
544 lines
19 KiB
C
544 lines
19 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_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);
|
|
}
|