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,328 @@
/*
* 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 "api.h"
#include <stddef.h>
#include <stdbool.h>
#include "app_db.h"
#include "app_glance_db.h"
#include "contacts_db.h"
#include "health_db.h"
#include "ios_notif_pref_db.h"
#include "notif_db.h"
#include "pin_db.h"
#include "prefs_db.h"
#include "reminder_db.h"
#include "watch_app_prefs_db.h"
#include "weather_db.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "system/logging.h"
typedef struct {
BlobDBInitImpl init;
BlobDBInsertImpl insert;
BlobDBGetLenImpl get_len;
BlobDBReadImpl read;
BlobDBDeleteImpl del;
BlobDBFlushImpl flush;
BlobDBIsDirtyImpl is_dirty;
BlobDBGetDirtyListImpl get_dirty_list;
BlobDBMarkSyncedImpl mark_synced;
bool disabled;
} BlobDB;
static const BlobDB s_blob_dbs[NumBlobDBs] = {
[BlobDBIdPins] = {
.init = pin_db_init,
.insert = pin_db_insert,
.get_len = pin_db_get_len,
.read = pin_db_read,
.del = pin_db_delete,
.flush = pin_db_flush,
.is_dirty = pin_db_is_dirty,
.get_dirty_list = pin_db_get_dirty_list,
.mark_synced = pin_db_mark_synced,
},
[BlobDBIdApps] = {
.init = app_db_init,
.insert = app_db_insert,
.get_len = app_db_get_len,
.read = app_db_read,
.del = app_db_delete,
.flush = app_db_flush,
},
[BlobDBIdReminders] = {
.init = reminder_db_init,
.insert = reminder_db_insert,
.get_len = reminder_db_get_len,
.read = reminder_db_read,
.del = reminder_db_delete,
.flush = reminder_db_flush,
.is_dirty = reminder_db_is_dirty,
.get_dirty_list = reminder_db_get_dirty_list,
.mark_synced = reminder_db_mark_synced,
},
[BlobDBIdNotifs] = {
.init = notif_db_init,
.insert = notif_db_insert,
.get_len = notif_db_get_len,
.read = notif_db_read,
.del = notif_db_delete,
.flush = notif_db_flush,
},
[BlobDBIdWeather] = {
#if CAPABILITY_HAS_WEATHER
.init = weather_db_init,
.insert = weather_db_insert,
.get_len = weather_db_get_len,
.read = weather_db_read,
.del = weather_db_delete,
.flush = weather_db_flush,
#else
.disabled = true,
#endif
},
[BlobDBIdiOSNotifPref] = {
.init = ios_notif_pref_db_init,
.insert = ios_notif_pref_db_insert,
.get_len = ios_notif_pref_db_get_len,
.read = ios_notif_pref_db_read,
.del = ios_notif_pref_db_delete,
.flush = ios_notif_pref_db_flush,
.is_dirty = ios_notif_pref_db_is_dirty,
.get_dirty_list = ios_notif_pref_db_get_dirty_list,
.mark_synced = ios_notif_pref_db_mark_synced,
},
[BlobDBIdPrefs] = {
.init = prefs_db_init,
.insert = prefs_db_insert,
.get_len = prefs_db_get_len,
.read = prefs_db_read,
.del = prefs_db_delete,
.flush = prefs_db_flush,
},
[BlobDBIdContacts] = {
#if !PLATFORM_TINTIN
.init = contacts_db_init,
.insert = contacts_db_insert,
.get_len = contacts_db_get_len,
.read = contacts_db_read,
.del = contacts_db_delete,
.flush = contacts_db_flush,
#else
// Disabled on tintin for code savings
.disabled = true,
#endif
},
[BlobDBIdWatchAppPrefs] = {
#if !PLATFORM_TINTIN
.init = watch_app_prefs_db_init,
.insert = watch_app_prefs_db_insert,
.get_len = watch_app_prefs_db_get_len,
.read = watch_app_prefs_db_read,
.del = watch_app_prefs_db_delete,
.flush = watch_app_prefs_db_flush,
#else
// Disabled on tintin for code savings
.disabled = true,
#endif
},
[BlobDBIdHealth] = {
#if CAPABILITY_HAS_HEALTH_TRACKING
.init = health_db_init,
.insert = health_db_insert,
.get_len = health_db_get_len,
.read = health_db_read,
.del = health_db_delete,
.flush = health_db_flush,
#else
.disabled = true,
#endif
},
[BlobDBIdAppGlance] = {
#if CAPABILITY_HAS_APP_GLANCES
.init = app_glance_db_init,
.insert = app_glance_db_insert,
.get_len = app_glance_db_get_len,
.read = app_glance_db_read,
.del = app_glance_db_delete,
.flush = app_glance_db_flush,
#else
.disabled = true,
#endif
},
};
static bool prv_db_valid(BlobDBId db_id) {
return (db_id < NumBlobDBs) && (!s_blob_dbs[db_id].disabled);
}
void blob_db_event_put(BlobDBEventType type, BlobDBId db_id, const uint8_t *key, int key_len) {
// copy key for event
uint8_t *key_bytes = NULL;
if (key_len > 0) {
key_bytes = kernel_malloc(key_len);
memcpy(key_bytes, key, key_len);
}
PebbleEvent e = {
.type = PEBBLE_BLOBDB_EVENT,
.blob_db = {
.db_id = db_id,
.type = type,
.key = key_bytes,
.key_len = (uint8_t)key_len,
}
};
event_put(&e);
}
void blob_db_init_dbs(void) {
const BlobDB *db = s_blob_dbs;
for (int i = 0; i < NumBlobDBs; ++i, ++db) {
if (db->init) {
db->init();
}
}
}
void blob_db_get_dirty_dbs(uint8_t *ids, uint8_t *num_ids) {
const BlobDB *db = s_blob_dbs;
*num_ids = 0;
for (uint8_t i = 0; i < NumBlobDBs; ++i, ++db) {
bool is_dirty = false;
if (db->is_dirty && (db->is_dirty(&is_dirty) == S_SUCCESS) && is_dirty) {
ids[*num_ids] = i;
*num_ids += 1;
}
}
}
status_t blob_db_insert(BlobDBId db_id,
const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
if (!prv_db_valid(db_id)) {
return E_RANGE;
}
const BlobDB *db = &s_blob_dbs[db_id];
if (db->insert) {
status_t rv = db->insert(key, key_len, val, val_len);
if (rv == S_SUCCESS) {
blob_db_event_put(BlobDBEventTypeInsert, db_id, key, key_len);
}
return rv;
}
return E_INVALID_OPERATION;
}
int blob_db_get_len(BlobDBId db_id,
const uint8_t *key, int key_len) {
if (!prv_db_valid(db_id)) {
return E_RANGE;
}
const BlobDB *db = &s_blob_dbs[db_id];
if (db->get_len) {
return db->get_len(key, key_len);
}
return E_INVALID_OPERATION;
}
status_t blob_db_read(BlobDBId db_id,
const uint8_t *key, int key_len, uint8_t *val_out, int val_len) {
if (!prv_db_valid(db_id)) {
return E_RANGE;
}
const BlobDB *db = &s_blob_dbs[db_id];
if (db->read) {
return db->read(key, key_len, val_out, val_len);
}
return E_INVALID_OPERATION;
}
status_t blob_db_delete(BlobDBId db_id,
const uint8_t *key, int key_len) {
if (!prv_db_valid(db_id)) {
return E_RANGE;
}
const BlobDB *db = &s_blob_dbs[db_id];
if (db->del) {
status_t rv = db->del(key, key_len);
if (rv == S_SUCCESS) {
blob_db_event_put(BlobDBEventTypeDelete, db_id, key, key_len);
}
return rv;
}
return E_INVALID_OPERATION;
}
status_t blob_db_flush(BlobDBId db_id) {
if (!prv_db_valid(db_id)) {
return E_RANGE;
}
const BlobDB *db = &s_blob_dbs[db_id];
if (db->flush) {
status_t rv = db->flush();
if (rv == S_SUCCESS) {
PBL_LOG(LOG_LEVEL_INFO, "Flushing BlobDB with Id %d", db_id);
blob_db_event_put(BlobDBEventTypeFlush, db_id, NULL, 0);
}
return rv;
}
return E_INVALID_OPERATION;
}
BlobDBDirtyItem *blob_db_get_dirty_list(BlobDBId db_id) {
if (!prv_db_valid(db_id)) {
return NULL;
}
const BlobDB *db = &s_blob_dbs[db_id];
if (db->get_dirty_list) {
return db->get_dirty_list();
}
return NULL;
}
status_t blob_db_mark_synced(BlobDBId db_id, uint8_t *key, int key_len) {
if (!prv_db_valid(db_id)) {
return E_RANGE;
}
const BlobDB *db = &s_blob_dbs[db_id];
if (db->mark_synced) {
status_t rv = db->mark_synced(key, key_len);
// TODO event?
return rv;
}
return E_INVALID_OPERATION;
}

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.
*/
#pragma once
#include "api_types.h"
#include <stdint.h>
#include <stdbool.h>
#include "system/status_codes.h"
#include "util/attributes.h"
#include "util/list.h"
#include "util/time/time.h"
//! The BlobDB API is a single consistent API to a number of key/value stores on the watch.
//! It is used in conjunction with the BlobDB endpoint.
//! Key/Value stores that are meant to be used with the endpoint need to implement this API
//! by implementing each of the Impl functions (see below).
//! A BlobDB is not guaranteed to persist across reboots, but it is guaranteed to
//! have executed a command when it returns a success code.
//! If you want to route commands to your BlobDB implementation API, you need
//! to add it to the \ref BlobDBId enum and to the BlobDBs list (\ref s_blob_dbs) in api.c
typedef enum PACKED {
BlobDBIdTest = 0x00,
BlobDBIdPins = 0x01,
BlobDBIdApps = 0x02,
BlobDBIdReminders = 0x03,
BlobDBIdNotifs = 0x04,
BlobDBIdWeather = 0x05,
BlobDBIdiOSNotifPref = 0x06,
BlobDBIdPrefs = 0x07,
BlobDBIdContacts = 0x08,
BlobDBIdWatchAppPrefs = 0x09,
BlobDBIdHealth = 0x0A,
BlobDBIdAppGlance = 0x0B,
NumBlobDBs,
} BlobDBId;
_Static_assert(sizeof(BlobDBId) == 1, "BlobDBId is larger than 1 byte");
//! A linked list of blob DB items that need to be synced
typedef struct {
ListNode node;
time_t last_updated;
int key_len; //!< length of the key, in bytes
uint8_t key[]; //!< key_len-size byte array of key data
} BlobDBDirtyItem;
//! A Blob DB's initialization routine.
//! This function will be called at boot when all blob dbs are init-ed
typedef void (*BlobDBInitImpl)(void);
//! Implements the insert API. Note that this function should be blocking.
//! \param key a pointer to the key data
//! \param key_len the lenght of the key, in bytes
//! \param val a pointer to the value data
//! \param val_len the length of the value, in bytes
//! \returns S_SUCCESS if the key/val pair was succesfully inserted
//! and an error code otherwise (See \ref StatusCode)
typedef status_t (*BlobDBInsertImpl)
(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
//! Implements the get length API.
//! \param key a pointer to the key data
//! \param key_len the lenght of the key, in bytes
//! \returns the length in bytes of the value for key on success
//! and an error code otherwise (See \ref StatusCode)
typedef int (*BlobDBGetLenImpl)
(const uint8_t *key, int key_len);
//! Implements the read API. Note that this function should be blocking.
//! \param key a pointer to the key data
//! \param key_len the lenght of the key, in bytes
//! \param[out] val_out a pointer to a buffer of size val_len
//! \param val_len the length of the value to be copied, in bytes
//! \returns S_SUCCESS if the value for key was succesfully read,
//! and an error code otherwise (See \ref StatusCode)
typedef status_t (*BlobDBReadImpl)
(const uint8_t *key, int key_len, uint8_t *val_out, int val_len);
//! Implements the delete API. Note that this function should be blocking.
//! \param key a pointer to the key data
//! \param key_len the lenght of the key, in bytes
//! \returns S_SUCCESS if the key/val pair was succesfully deleted
//! and an error code otherwise (See \ref StatusCode)
typedef status_t (*BlobDBDeleteImpl)
(const uint8_t *key, int key_len);
//! Implements the flush API. Note that this function should be blocking.
//! \returns S_SUCCESS if all key/val pairs were succesfully deleted
//! and an error code otherwise (See \ref StatusCode)
typedef status_t (*BlobDBFlushImpl)(void);
//! Implements the IsDirty API.
//! \param[out] is_dirty_out reference to a boolean that will be set depending on the DB state
//! \note if the function does not return S_SUCCESS, the state of the boolean is undefined.
//! \returns S_SUCCESS if the query succeeded, an error code otherwise
typedef status_t (*BlobDBIsDirtyImpl)(bool *is_dirty_out);
//! Implements the GetDirtyList API.
//! \return a linked list of \ref BlobDBDirtyItem with a node per out-of-sync item.
//! \note There is no limit how large this list can get. Handle OOM scenarios gracefully!
typedef BlobDBDirtyItem *(*BlobDBGetDirtyListImpl)(void);
//! Implements the MarkSynced API.
//! \param key a pointer to the key data
//! \param key_len the lenght of the key, in bytes
//! \returns S_SUCCESS if the item was marked synced, an error code otherwise
typedef status_t (*BlobDBMarkSyncedImpl)(const uint8_t *key, int key_len);
//! Emits a Blob DB event.
//! \param type The type of event to emit
//! \param db_id the ID of the blob DB
//! \param key a pointer to the key data
//! \param key_len the length of the key, in bytes
void blob_db_event_put(BlobDBEventType type, BlobDBId db_id, const uint8_t *key, int key_len);
//! Call the BlobDBInitImpl for all the databases
void blob_db_init_dbs(void);
//! Call the BlobDBIsDirtyImpl for each database, and fill the 'ids' list
//! with all the dirty DB ids
//! \param[out] ids an array of BlobDbIds of size NumBlobDBs or more.
//! \param[out] num_ids an array of BlobDbIds of size NumBlobDBs or more.
//! \note The unused entries will be set to 0.
void blob_db_get_dirty_dbs(uint8_t *ids, uint8_t *num_ids);
//! Insert a key/val pair in a blob DB.
//! See \ref BlobDBReadImpl
//! \param db_id the ID of the blob DB
//! \param key a pointer to the key data
//! \param key_len the lenght of the key, in bytes
status_t blob_db_insert(BlobDBId db_id,
const uint8_t *key, int key_len, const uint8_t *val, int val_len);
//! Get the length of the value in a blob DB for a given key.
//! See \ref BlobDBGetLenImpl
//! \param db_id the ID of the blob DB
//! \param key a pointer to the key data
//! \param key_len the lenght of the key, in bytes
int blob_db_get_len(BlobDBId db_id,
const uint8_t *key, int key_len);
//! Get the value of length val_len for a given key
//! \param db_id the ID of the blob DB
//! See \ref BlobDBReadImpl
status_t blob_db_read(BlobDBId db_id,
const uint8_t *key, int key_len, uint8_t *val_out, int val_len);
//! Delete the key/val pair in a blob DB for a given key
//! \param db_id the ID of the blob DB
//! See \ref BlobDBDeleteImpl
status_t blob_db_delete(BlobDBId db_id,
const uint8_t *key, int key_len);
//! Delete all key/val pairs in a blob DB.
//! \param db_id the ID of the blob DB
//! See \ref BlobDBFlushImpl
status_t blob_db_flush(BlobDBId db_id);
//! Get the list of items in a given blob DB that have yet to be synced.
//! Items originating from the phone are always marked as synced.
//! \note Use the APIs in sync.h to initiate a sync.
//! \param db_id the ID of the blob DB
//! \see BlobDBGetDirtyListImpl
BlobDBDirtyItem *blob_db_get_dirty_list(BlobDBId db_id);
//! Mark an item in a blob DB as having been synced
//! \note This API is used upon receiving an ACK from the phone during sync
//! \param db_id the ID of the blob DB
//! \see BlobDBMarkSyncedImpl
status_t blob_db_mark_synced(BlobDBId db_id, uint8_t *key, int key_len);

View file

@ -0,0 +1,23 @@
/*
* 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 enum BlobDBEventType {
BlobDBEventTypeInsert,
BlobDBEventTypeDelete,
BlobDBEventTypeFlush,
} BlobDBEventType;

View file

@ -0,0 +1,424 @@
/*
* 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_db.h"
#include "util/uuid.h"
#include "kernel/pbl_malloc.h"
#include "process_management/app_install_manager_private.h"
#include "services/normal/filesystem/pfs.h"
#include "services/normal/settings/settings_file.h"
#include "services/normal/app_fetch_endpoint.h"
#include "os/mutex.h"
#include "system/logging.h"
#include "system/passert.h"
#include "system/status_codes.h"
#include "util/math.h"
#include "util/units.h"
#define SETTINGS_FILE_NAME "appdb"
// Holds about ~150 app metadata blobs
#define SETTINGS_FILE_SIZE KiBYTES(20)
#define FIRST_VALID_INSTALL_ID (INSTALL_ID_INVALID + 1)
static AppInstallId s_next_unique_flash_app_id;
static struct {
SettingsFile settings_file;
PebbleMutex *mutex;
} s_app_db;
//////////////////////
// Settings helpers
//////////////////////
struct AppDBInitData {
AppInstallId max_id;
uint32_t num_apps;
};
static status_t prv_lock_mutex_and_open_file(void) {
mutex_lock(s_app_db.mutex);
status_t rv = settings_file_open(&s_app_db.settings_file,
SETTINGS_FILE_NAME,
SETTINGS_FILE_SIZE);
if (rv != S_SUCCESS) {
mutex_unlock(s_app_db.mutex);
}
return rv;
}
static void prv_close_file_and_unlock_mutex(void) {
settings_file_close(&s_app_db.settings_file);
mutex_unlock(s_app_db.mutex);
}
static status_t prv_cancel_app_fetch(AppInstallId app_id) {
if (pebble_task_get_current() == PebbleTask_KernelBackground) {
// if we are on kernel_bg, we can go ahead and cancel the app fetch instantly
app_fetch_cancel_from_system_task(app_id);
return S_SUCCESS;
} else {
// ignore the deletion and send back a failure message. The phone will retry later.
return E_BUSY;
}
}
//! SettingsFileEachCallback function is used to iterate over all keys and find the largest
//! AppInstallId currently being using.
static bool prv_each_inspect_ids(SettingsFile *file, SettingsRecordInfo *info, void *context) {
// check entry is valid
if ((info->val_len == 0) || (info->key_len != sizeof(AppInstallId))) {
return true; // continue iterating
}
struct AppDBInitData *data = context;
AppInstallId app_id;
info->get_key(file, (uint8_t *)&app_id, sizeof(AppInstallId));
data->max_id = MAX(data->max_id, app_id);
data->num_apps++;
return true; // continue iterating
}
struct UuidFilterData {
Uuid uuid;
AppInstallId found_id;
};
//! SettingsFileEachCallback function is used to iterate over all entries and search for
//! the particular entry with the given UUID. If one is found, it will set the uuid_data->found_id
//! to a value other than INSTALL_ID_INVALID
static bool prv_db_filter_app_id(SettingsFile *file, SettingsRecordInfo *info, void *context) {
// check entry is valid
if ((info->val_len == 0) || (info->key_len != sizeof(AppInstallId))) {
return true; // continue iterating
}
struct UuidFilterData *uuid_data = (struct UuidFilterData *)context;
AppInstallId app_id;
AppDBEntry entry;
info->get_key(file, (uint8_t *)&app_id, info->key_len);
info->get_val(file, (uint8_t *)&entry, info->val_len);
if (uuid_equal(&uuid_data->uuid, &entry.uuid)) {
uuid_data->found_id = app_id;
return false; // stop iterating
}
return true; // continue iterating
}
//! Retrieves the AppInstallId for a given UUID using the SettingsFile that is already open.
//! @note Requires holding the lock already
static AppInstallId prv_find_install_id_for_uuid(SettingsFile *file, const Uuid *uuid) {
// used when iterating through all entries in our database.
struct UuidFilterData filter_data = {
.found_id = INSTALL_ID_INVALID,
.uuid = *uuid,
};
settings_file_each(file, prv_db_filter_app_id, (void *)&filter_data);
return filter_data.found_id;
}
/////////////////////////
// App DB Specific API
/////////////////////////
AppInstallId app_db_get_install_id_for_uuid(const Uuid *uuid) {
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
AppInstallId app_id = prv_find_install_id_for_uuid(&s_app_db.settings_file, uuid);
prv_close_file_and_unlock_mutex();
return app_id;
}
///////////////////////////
// App DB API
///////////////////////////
//! Fills an AppDBEntry for a given UUID. This is a wrapper around app_db_read to keep it uniform
//! with `app_db_get_app_entry_for_install_id`
status_t app_db_get_app_entry_for_uuid(const Uuid *uuid, AppDBEntry *entry) {
return app_db_read((uint8_t *)uuid, sizeof(Uuid), (uint8_t *)entry, sizeof(AppDBEntry));
}
status_t app_db_get_app_entry_for_install_id(AppInstallId app_id, AppDBEntry *entry) {
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_get(&s_app_db.settings_file, (uint8_t *)&app_id, sizeof(AppInstallId),
(uint8_t *)entry, sizeof(AppDBEntry));
prv_close_file_and_unlock_mutex();
return rv;
}
bool app_db_exists_install_id(AppInstallId app_id) {
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
bool exists = settings_file_exists(&s_app_db.settings_file, (uint8_t *)&app_id,
sizeof(AppInstallId));
prv_close_file_and_unlock_mutex();
return exists;
}
typedef struct {
AppDBEnumerateCb cb;
void *data;
AppDBEntry *entry_buf;
} EnumerateData;
static bool prv_enumerate_entries(SettingsFile *file, SettingsRecordInfo *info, void *context) {
// check entry is valid
if ((info->val_len == 0) || (info->key_len != sizeof(AppInstallId))) {
return true; // continue iteration
}
EnumerateData *cb_data = (EnumerateData *)context;
AppInstallId id;
info->get_key(file, (uint8_t *)&id, info->key_len);
info->get_val(file, (uint8_t *)cb_data->entry_buf, info->val_len);
// check return value
cb_data->cb(id, cb_data->entry_buf, cb_data->data);
return true; // continue iteration
}
void app_db_enumerate_entries(AppDBEnumerateCb cb, void *data) {
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return;
}
AppDBEntry *db_entry = kernel_malloc_check(sizeof(AppDBEntry));
EnumerateData cb_data = {
.cb = cb,
.data = data,
.entry_buf = db_entry,
};
settings_file_each(&s_app_db.settings_file, prv_enumerate_entries, &cb_data);
prv_close_file_and_unlock_mutex();
kernel_free(db_entry);
return;
}
/////////////////////////
// Blob DB API
/////////////////////////
void app_db_init(void) {
memset(&s_app_db, 0, sizeof(s_app_db));
s_app_db.mutex = mutex_create();
// set to zero to reset unit test static variable.
s_next_unique_flash_app_id = INSTALL_ID_INVALID;
// Iterate through all entires and find the one with the highest AppInstallId. The next unique
// is then one greater than the largest found.
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
WTF;
}
struct AppDBInitData data = { 0 };
settings_file_each(&s_app_db.settings_file, prv_each_inspect_ids, &data);
if (data.max_id == INSTALL_ID_INVALID) {
s_next_unique_flash_app_id = (INSTALL_ID_INVALID + 1);
} else {
s_next_unique_flash_app_id = (data.max_id + 1);
}
PBL_LOG(LOG_LEVEL_INFO, "Found %"PRIu32" apps. Next ID: %"PRIu32" ", data.num_apps,
s_next_unique_flash_app_id);
prv_close_file_and_unlock_mutex();
}
status_t app_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
if (key_len != UUID_SIZE ||
val_len != sizeof(AppDBEntry)) {
return E_INVALID_ARGUMENT;
}
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
PBL_ASSERTN(key_len == 16);
PBL_ASSERTN(val_len > 0);
bool new_install = false;
AppInstallId app_id = prv_find_install_id_for_uuid(&s_app_db.settings_file, (const Uuid *)key);
if (app_id == INSTALL_ID_INVALID) {
new_install = true;
app_id = s_next_unique_flash_app_id++;
} else if (app_fetch_in_progress()) {
PBL_LOG(LOG_LEVEL_WARNING, "Got an insert for an app that is currently being fetched, %"PRId32,
app_id);
rv = prv_cancel_app_fetch(app_id);
}
if (rv == S_SUCCESS) {
rv = settings_file_set(&s_app_db.settings_file, (uint8_t *)&app_id,
sizeof(AppInstallId), val, val_len);
}
prv_close_file_and_unlock_mutex();
if (rv == S_SUCCESS) {
// app install something
app_install_do_callbacks(new_install ? APP_AVAILABLE : APP_UPGRADED, app_id, NULL, NULL, NULL);
}
return rv;
}
int app_db_get_len(const uint8_t *key, int key_len) {
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
PBL_ASSERTN(key_len == 16);
// should not increment !!!!
AppInstallId app_id = prv_find_install_id_for_uuid(&s_app_db.settings_file, (Uuid *)key);
if (app_id == INSTALL_ID_INVALID) {
rv = 0;
} else {
rv = settings_file_get_len(&s_app_db.settings_file, (uint8_t *)&app_id, sizeof(AppInstallId));
}
prv_close_file_and_unlock_mutex();
return rv;
}
status_t app_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_len) {
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
PBL_ASSERTN(key_len == 16);
AppInstallId app_id = prv_find_install_id_for_uuid(&s_app_db.settings_file, (Uuid *)key);
if (app_id == INSTALL_ID_INVALID) {
rv = E_DOES_NOT_EXIST;
} else {
rv = settings_file_get(&s_app_db.settings_file, (uint8_t *)&app_id,
sizeof(AppInstallId), val_out, val_len);
}
prv_close_file_and_unlock_mutex();
return rv;
}
status_t app_db_delete(const uint8_t *key, int key_len) {
if (key_len != UUID_SIZE) {
return E_INVALID_ARGUMENT;
}
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
PBL_ASSERTN(key_len == 16);
AppInstallId app_id = prv_find_install_id_for_uuid(&s_app_db.settings_file, (Uuid *)key);
if (app_id == INSTALL_ID_INVALID) {
rv = E_DOES_NOT_EXIST;
} else if (app_fetch_in_progress()) {
PBL_LOG(LOG_LEVEL_WARNING, "Tried to delete an app that is currently being fetched, %"PRId32,
app_id);
rv = prv_cancel_app_fetch(app_id);
}
if (rv == S_SUCCESS) {
rv = settings_file_delete(&s_app_db.settings_file, (uint8_t *)&app_id, sizeof(AppInstallId));
}
prv_close_file_and_unlock_mutex();
if (rv == S_SUCCESS) {
// uuid will be free'd by app_install_manager
Uuid *uuid_copy = kernel_malloc_check(sizeof(Uuid));
memcpy(uuid_copy, key, sizeof(Uuid));
app_install_do_callbacks(APP_REMOVED, app_id, uuid_copy, NULL, NULL);
}
return rv;
}
status_t app_db_flush(void) {
PBL_LOG(LOG_LEVEL_WARNING, "AppDB Flush initiated");
if (app_fetch_in_progress()) {
// cancels any app fetch
status_t rv = prv_cancel_app_fetch(INSTALL_ID_INVALID);
if (rv != S_SUCCESS) {
return rv;
}
}
app_install_do_callbacks(APP_DB_CLEARED, INSTALL_ID_INVALID, NULL, NULL, NULL);
// let app install manager deal with deleting the cache and removing related timeline pins
app_install_clear_app_db();
// remove the settings file
mutex_lock(s_app_db.mutex);
pfs_remove(SETTINGS_FILE_NAME);
mutex_unlock(s_app_db.mutex);
PBL_LOG(LOG_LEVEL_WARNING, "AppDB Flush finished");
return S_SUCCESS;
}
//////////////////////
// Test functions
//////////////////////
// automated testing and app_install_manager prompt commands
int32_t app_db_check_next_unique_id(void) {
return s_next_unique_flash_app_id;
}

View file

@ -0,0 +1,76 @@
/*
* 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 "util/uuid.h"
#include "process_management/app_install_manager.h"
#include "process_management/pebble_process_info.h"
#include "system/status_codes.h"
#include "util/attributes.h"
#include "util/list.h"
//! App database entry for BlobDB. First pass is very basic. The list will expand as more features
//! and requirements are implemented.
typedef struct PACKED {
Uuid uuid;
uint32_t info_flags;
uint32_t icon_resource_id;
Version app_version;
Version sdk_version;
GColor8 app_face_bg_color;
uint8_t template_id;
char name[APP_NAME_SIZE_BYTES];
} AppDBEntry;
//! Used in app_db_enumerate_entries
typedef void(*AppDBEnumerateCb)(AppInstallId install_id, AppDBEntry *entry, void *data);
/* AppDB Functions */
int32_t app_db_get_next_unique_id(void);
AppInstallId app_db_get_install_id_for_uuid(const Uuid *uuid);
status_t app_db_get_app_entry_for_uuid(const Uuid *uuid, AppDBEntry *entry);
status_t app_db_get_app_entry_for_install_id(AppInstallId app_id, AppDBEntry *entry);
void app_db_enumerate_entries(AppDBEnumerateCb cb, void *data);
/* AppDB AppInstallId Implementation */
bool app_db_exists_install_id(AppInstallId app_id);
/* BlobDB Implementation */
void app_db_init(void);
status_t app_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
int app_db_get_len(const uint8_t *key, int key_len);
status_t app_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
status_t app_db_delete(const uint8_t *key, int key_len);
status_t app_db_flush(void);
/* TEST */
AppInstallId app_db_check_next_unique_id(void);

View file

@ -0,0 +1,860 @@
/*
* 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_glance_db.h"
#include "app_glance_db_private.h"
#include "applib/app_glance.h"
#include "drivers/rtc.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "os/mutex.h"
#include "resource/resource_ids.auto.h"
#include "process_management/app_install_manager.h"
#include "services/normal/app_cache.h"
#include "services/normal/filesystem/pfs.h"
#include "services/normal/settings/settings_file.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/math.h"
#include "util/units.h"
#define SETTINGS_FILE_NAME "appglancedb"
//! The defines below calculate `APP_GLANCE_DB_MAX_USED_SIZE` which is the actual minimum space we
//! need to guarantee all of the apps's glances on the watch can have the same number of slices,
//! and that number currently evaluates to 69050 bytes. We provide some additional space beyond that
//! for some safety margin and easy future expansion, and thus use 80KB for the settings file size.
#define SETTINGS_FILE_SIZE (KiBYTES(80))
#define APP_GLANCE_DB_GLANCE_MAX_SIZE \
(sizeof(SerializedAppGlanceHeader) + \
(APP_GLANCE_DB_SLICE_MAX_SIZE * APP_GLANCE_DB_MAX_SLICES_PER_GLANCE))
#define APP_GLANCE_DB_MAX_USED_SIZE \
(APP_GLANCE_DB_GLANCE_MAX_SIZE * APP_GLANCE_DB_MAX_NUM_APP_GLANCES)
_Static_assert(APP_GLANCE_DB_MAX_USED_SIZE <= SETTINGS_FILE_SIZE, "AppGlanceDB is too small!");
static struct {
SettingsFile settings_file;
PebbleMutex *mutex;
} s_app_glance_db;
//////////////////////////////////////////
// Slice Type Implementation Definition
//////////////////////////////////////////
//! Return true if the type-specific serialized slice's attribute list is valid. You don't have to
//! check the attribute list pointer (we check it before calling this callback).
typedef bool (*AttributeListValidationFunc)(const AttributeList *attr_list);
//! Callback for copying the type-specific attributes from a serialized slice's attribute list
//! to the provided slice. You can assume that the attribute list and the slice_out pointers are
//! valid because we check them before calling this callback.
typedef void (*InitSliceFromAttributeListFunc)(const AttributeList *attr_list,
AppGlanceSliceInternal *slice_out);
//! Callback for adding the type-specific fields from a slice to the provided attribute list.
//! You can assume that the slice and attribute list pointers are valid because we check them
//! before calling this callback.
typedef void (*InitAttributeListFromSliceFunc)(const AppGlanceSliceInternal *slice,
AttributeList *attr_list_to_init);
typedef struct SliceTypeImplementation {
AttributeListValidationFunc is_attr_list_valid;
InitSliceFromAttributeListFunc init_slice_from_attr_list;
InitAttributeListFromSliceFunc init_attr_list_from_slice;
} SliceTypeImplementation;
////////////////////////////////////////////////////////
// AppGlanceSliceType_IconAndSubtitle Implementation
////////////////////////////////////////////////////////
static bool prv_is_icon_and_subtitle_slice_attribute_list_valid(const AttributeList *attr_list) {
// The icon and subtitle are optional.
return true;
}
static void prv_init_icon_and_subtitle_slice_from_attr_list(const AttributeList *attr_list,
AppGlanceSliceInternal *slice_out) {
slice_out->icon_and_subtitle.icon_resource_id = attribute_get_uint32(attr_list,
AttributeIdIcon,
INVALID_RESOURCE);
strncpy(slice_out->icon_and_subtitle.template_string,
attribute_get_string(attr_list, AttributeIdSubtitleTemplateString, NULL),
ATTRIBUTE_APP_GLANCE_SUBTITLE_MAX_LEN + 1);
}
static void prv_init_attribute_list_from_icon_and_subtitle_slice(
const AppGlanceSliceInternal *slice, AttributeList *attr_list_to_init) {
attribute_list_add_cstring(attr_list_to_init, AttributeIdSubtitleTemplateString,
slice->icon_and_subtitle.template_string);
attribute_list_add_resource_id(attr_list_to_init, AttributeIdIcon,
slice->icon_and_subtitle.icon_resource_id);
}
//////////////////////////////////
// Slice Type Implementations
//////////////////////////////////
//! Add new entries to this array as we introduce new slice types
static const SliceTypeImplementation s_slice_type_impls[AppGlanceSliceTypeCount] = {
[AppGlanceSliceType_IconAndSubtitle] = {
.is_attr_list_valid = prv_is_icon_and_subtitle_slice_attribute_list_valid,
.init_slice_from_attr_list = prv_init_icon_and_subtitle_slice_from_attr_list,
.init_attr_list_from_slice = prv_init_attribute_list_from_icon_and_subtitle_slice,
},
};
//////////////////////////////////
// Serialized Slice Iteration
//////////////////////////////////
//! Return true to continue iteration and false to stop it.
typedef bool (*SliceForEachCb)(SerializedAppGlanceSliceHeader *serialized_slice, void *context);
//! Returns true if iteration completed successfully, either due to reaching the end of the slices
//! or if the client's callback returns false to stop iteration early.
//! Returns false if an error occurred during iteration due to the slices' `.total_size` values
//! not being consistent with the provided `serialized_glance_size` argument.
static bool prv_slice_for_each(SerializedAppGlanceHeader *serialized_glance,
size_t serialized_glance_size, SliceForEachCb cb, void *context) {
if (!serialized_glance || !cb) {
return false;
}
SerializedAppGlanceSliceHeader *current_slice =
(SerializedAppGlanceSliceHeader *)serialized_glance->data;
size_t glance_size_processed = sizeof(SerializedAppGlanceHeader);
// Note that we'll stop iterating after reading the max supported number of slices per glance
for (unsigned int i = 0; i < APP_GLANCE_DB_MAX_SLICES_PER_GLANCE; i++) {
// Stop iterating if we've read all of the slices by hitting the end of the glance data
if (glance_size_processed == serialized_glance_size) {
break;
}
// Stop iterating and report an error if we've somehow gone beyond the end of the glance data
if (glance_size_processed > serialized_glance_size) {
return false;
}
// Stop iterating if the client's callback function returns false
if (!cb(current_slice, context)) {
break;
}
// Advance to the next slice
glance_size_processed += current_slice->total_size;
current_slice =
(SerializedAppGlanceSliceHeader *)(((uint8_t *)current_slice) + current_slice->total_size);
}
return true;
}
/////////////////////////////////////////
// Serialized Slice Validation Helpers
/////////////////////////////////////////
static bool prv_is_slice_type_valid(uint8_t type) {
return (type < AppGlanceSliceTypeCount);
}
//! Returns true if the provided AttributeList is valid for the specified AppGlanceSliceType,
//! false otherwise.
static bool prv_is_slice_attribute_list_valid(uint8_t type, const AttributeList *attr_list) {
// Check if the slice type is valid before we plug it into the validation func array below
if (!prv_is_slice_type_valid(type)) {
return false;
}
// Check if the AttributeList has the attributes required for this specific slice type
return s_slice_type_impls[type].is_attr_list_valid(attr_list);
}
//////////////////////////////////
// Slice Deserialization
//////////////////////////////////
//! Returns true if a non-empty AttributeList was successfully deserialized from `serialized_slice`,
//! `attr_list` was filled with the result, and `attr_list_data_buffer_out` was filled with the data
//! buffer for `attr_list_out`. Returns false otherwise.
//! @note If function returns true, client must call `attribute_list_destroy_list()`
//! on `attr_list_out` and `kernel_free()` on `attr_list_data_buffer_out`.
static bool prv_deserialize_attribute_list(const SerializedAppGlanceSliceHeader *serialized_slice,
AttributeList *attr_list_out,
char **attr_list_data_buffer_out) {
if (!serialized_slice || !attr_list_out || !attr_list_data_buffer_out) {
return false;
}
const uint8_t num_attributes = serialized_slice->num_attributes;
// If there aren't any attributes, set `attr_list_out` to be an empty AttributeList and return
// true because technically we did successfully deserialize the AttributeList
if (!num_attributes) {
*attr_list_out = (AttributeList) {};
*attr_list_data_buffer_out = NULL;
return true;
}
const uint8_t * const serialized_attr_list_start = serialized_slice->data;
const uint8_t * const serialized_attr_list_end =
serialized_attr_list_start + serialized_slice->total_size;
// Get the buffer size needed for the attributes we're going to deserialize
const uint8_t *buffer_size_cursor = serialized_attr_list_start;
const int32_t buffer_size =
attribute_get_buffer_size_for_serialized_attributes(num_attributes, &buffer_size_cursor,
serialized_attr_list_end);
if (buffer_size < 0) {
PBL_LOG(LOG_LEVEL_WARNING,
"Failed to measure the buffer size required for deserializing an AttributeList from a "
"serialized slice");
return false;
}
if (buffer_size) {
// Allocate buffer for the data attached to the attributes
*attr_list_data_buffer_out = kernel_zalloc(buffer_size);
if (!*attr_list_data_buffer_out) {
PBL_LOG(LOG_LEVEL_ERROR,
"Failed to alloc memory for the Attributes' data buffer while deserializing an "
"AttributeList from a serialized slice");
return false;
}
} else {
// No buffer needed, but set the output pointer to NULL because we might blindly free it below
// if we fail to alloc memory for the attribute_buffer
*attr_list_data_buffer_out = NULL;
}
// Allocate buffer for the Attribute's
// Note that this doesn't need to be passed back as an output because it gets freed as part of the
// client calling `attribute_list_destroy_list()` on `attr_list_out`
Attribute *attribute_buffer = kernel_zalloc(num_attributes * sizeof(*attribute_buffer));
if (!attribute_buffer) {
PBL_LOG(LOG_LEVEL_ERROR,
"Failed to alloc memory for the buffer of Attribute's while deserializing an "
"AttributeList from a serialized slice");
// Free the `*attr_list_data_buffer_out` we might have allocated above
kernel_free(*attr_list_data_buffer_out);
return false;
}
// Setup the arguments for `attribute_deserialize_list()`
char *attribute_data_buffer_pointer = *attr_list_data_buffer_out;
char * const attribute_data_buffer_end = attribute_data_buffer_pointer + buffer_size;
*attr_list_out = (AttributeList) {
.num_attributes = num_attributes,
.attributes = attribute_buffer,
};
const uint8_t *deserialization_cursor = serialized_attr_list_start;
// Try to deserialize the AttributeList
const bool was_attr_list_deserialized = attribute_deserialize_list(&attribute_data_buffer_pointer,
attribute_data_buffer_end,
&deserialization_cursor,
serialized_attr_list_end,
*attr_list_out);
if (!was_attr_list_deserialized) {
kernel_free(attribute_buffer);
kernel_free(*attr_list_data_buffer_out);
}
return was_attr_list_deserialized;
}
typedef struct SliceDeserializationIteratorContext {
AppGlance *glance_out;
bool deserialization_failed;
} SliceDeserializationIteratorContext;
static bool prv_deserialize_slice(SerializedAppGlanceSliceHeader *serialized_slice, void *context) {
SliceDeserializationIteratorContext *deserialization_context = context;
// Deserialize the serialized slice's attribute list
AttributeList attr_list = {};
char *attr_list_data_buffer = NULL;
if (!prv_deserialize_attribute_list(serialized_slice, &attr_list, &attr_list_data_buffer)) {
deserialization_context->deserialization_failed = true;
return false;
}
// Check that the deserialized attribute list is valid
const bool success = prv_is_slice_attribute_list_valid(serialized_slice->type, &attr_list);
if (!success) {
goto cleanup;
}
AppGlance *glance_out = deserialization_context->glance_out;
// Copy the common serialized slice fields to the output glance's slice
const unsigned int current_slice_index = glance_out->num_slices;
AppGlanceSliceInternal *current_slice_out = &glance_out->slices[current_slice_index];
// Note that we default the expiration time to "never expire" if one was not provided
*current_slice_out = (AppGlanceSliceInternal) {
.expiration_time = attribute_get_uint32(&attr_list, AttributeIdTimestamp,
APP_GLANCE_SLICE_NO_EXPIRATION),
.type = (AppGlanceSliceType)serialized_slice->type,
};
// Copy type-specific fields from the serialized slice to the output glance's slice
s_slice_type_impls[serialized_slice->type].init_slice_from_attr_list(&attr_list,
current_slice_out);
// Increment the number of slices in the glance
glance_out->num_slices++;
cleanup:
attribute_list_destroy_list(&attr_list);
kernel_free(attr_list_data_buffer);
return success;
}
static status_t prv_deserialize_glance(SerializedAppGlanceHeader *serialized_glance,
size_t serialized_glance_size, AppGlance *glance_out) {
if (!serialized_glance || !glance_out) {
return E_INVALID_ARGUMENT;
}
// Zero out the output glance
*glance_out = (AppGlance) {};
// Iterate over the slices to deserialize them
SliceDeserializationIteratorContext context = (SliceDeserializationIteratorContext) {
.glance_out = glance_out,
};
if (!prv_slice_for_each(serialized_glance, serialized_glance_size,
prv_deserialize_slice, &context) ||
context.deserialization_failed) {
return E_ERROR;
}
return S_SUCCESS;
}
//////////////////////////////////
// Slice Serialization
//////////////////////////////////
typedef struct SliceSerializationAttributeListData {
AttributeList attr_list;
size_t attr_list_size;
} SliceSerializationAttributeListData;
//! Returns S_SUCCESS if the provided glance was successfully serialized into serialized_glance_out
//! and its serialized size copied to serialized_glance_size_out.
//! @note If function returns S_SUCCESS, client must call `kernel_free()` on the pointer provided
//! for `serialized_glance_out`.
static status_t prv_serialize_glance(const AppGlance *glance,
SerializedAppGlanceHeader **serialized_glance_out,
size_t *serialized_glance_size_out) {
if (!glance || (glance->num_slices > APP_GLANCE_DB_MAX_SLICES_PER_GLANCE) ||
!serialized_glance_out || !serialized_glance_size_out) {
return E_INVALID_ARGUMENT;
}
// Allocate a buffer for data about each slice's attribute list, but only if we have at least
// one slice because allocating 0 bytes would return NULL and that is a return value we want to
// reserve for the case when we've run out of memory
SliceSerializationAttributeListData *attr_lists = NULL;
if (glance->num_slices > 0) {
attr_lists = kernel_zalloc(sizeof(SliceSerializationAttributeListData) * glance->num_slices);
if (!attr_lists) {
return E_OUT_OF_MEMORY;
}
}
status_t rv;
// Iterate over the glance slices, creating attribute lists and summing the size we need for the
// overall serialized slice
size_t serialized_glance_size = sizeof(SerializedAppGlanceHeader);
for (unsigned int slice_index = 0; slice_index < glance->num_slices; slice_index++) {
SliceSerializationAttributeListData *current_attr_list_data = &attr_lists[slice_index];
const AppGlanceSliceInternal *current_slice = &glance->slices[slice_index];
// Check the slice's type, fail the entire serialization if it's invalid
if (!prv_is_slice_type_valid(current_slice->type)) {
PBL_LOG(LOG_LEVEL_WARNING,
"Tried to serialize a glance containing a slice with invalid type: %d",
current_slice->type);
rv = E_INVALID_ARGUMENT;
goto cleanup;
}
serialized_glance_size += sizeof(SerializedAppGlanceSliceHeader);
AttributeList *attr_list = &current_attr_list_data->attr_list;
// Initialize the attributes common to all slice types in the attribute list
attribute_list_add_uint32(attr_list, AttributeIdTimestamp,
(uint32_t)current_slice->expiration_time);
// Initialize the type-specific attributes in the attribute list
s_slice_type_impls[current_slice->type].init_attr_list_from_slice(current_slice, attr_list);
// Record size of the attribute list in the data struct as well as the overall size accumulator
current_attr_list_data->attr_list_size = attribute_list_get_serialized_size(attr_list);
serialized_glance_size += current_attr_list_data->attr_list_size;
}
// Allocate a buffer for the serialized glance
SerializedAppGlanceHeader *serialized_glance = kernel_zalloc(serialized_glance_size);
if (!serialized_glance) {
rv = E_OUT_OF_MEMORY;
goto cleanup;
}
// Populate the header of the serialized glance
*serialized_glance = (SerializedAppGlanceHeader) {
.version = APP_GLANCE_DB_CURRENT_VERSION,
.creation_time = (uint32_t)rtc_get_time(),
};
uint8_t *glance_buffer_start = (uint8_t *)serialized_glance;
uint8_t *glance_buffer_end = glance_buffer_start + serialized_glance_size;
// Start the cursor where the serialized slices go
uint8_t *glance_buffer_cursor = serialized_glance->data;
// Serialize each slice into the serialized glance buffer
for (unsigned int slice_index = 0; slice_index < glance->num_slices; slice_index++) {
const AppGlanceSliceInternal *current_slice = &glance->slices[slice_index];
SliceSerializationAttributeListData *current_attr_list_data = &attr_lists[slice_index];
AttributeList *attr_list = &current_attr_list_data->attr_list;
const size_t attr_list_size = current_attr_list_data->attr_list_size;
// Calculate the total size of this serialized slice
const uint16_t serialized_slice_total_size =
sizeof(SerializedAppGlanceSliceHeader) + attr_list_size;
// Populate the serialized slice header
SerializedAppGlanceSliceHeader *serialized_slice_header =
(SerializedAppGlanceSliceHeader *)glance_buffer_cursor;
*serialized_slice_header = (SerializedAppGlanceSliceHeader) {
.type = current_slice->type,
.total_size = serialized_slice_total_size,
.num_attributes = attr_list->num_attributes,
};
// Serialize the slice's attribute list
attribute_list_serialize(attr_list, serialized_slice_header->data, glance_buffer_end);
// Note that we'll destroy the attribute list's attributes below in the cleanup section
// Advance the cursor by the serialized slice's total size
glance_buffer_cursor += serialized_slice_total_size;
}
// Check that we fully populated the serialized glance buffer
rv = (glance_buffer_cursor == glance_buffer_end) ? S_SUCCESS : E_ERROR;
if (rv == S_SUCCESS) {
*serialized_glance_out = serialized_glance;
*serialized_glance_size_out = serialized_glance_size;
} else {
kernel_free(serialized_glance);
}
cleanup:
// Destroy the attributes of each of the attribute lists in attr_lists
for (unsigned int i = 0; i < glance->num_slices; i++) {
attribute_list_destroy_list(&attr_lists[i].attr_list);
}
kernel_free(attr_lists);
return rv;
}
//////////////////////////////////
// Serialized Slice Validation
//////////////////////////////////
static bool prv_is_serialized_slice_valid(const SerializedAppGlanceSliceHeader *serialized_slice) {
if (!serialized_slice ||
!prv_is_slice_type_valid(serialized_slice->type) ||
!WITHIN(serialized_slice->total_size, APP_GLANCE_DB_SLICE_MIN_SIZE,
APP_GLANCE_DB_SLICE_MAX_SIZE)) {
return false;
}
// Deserialize the AttributeList from `serialized_slice`
AttributeList attr_list = {};
char *attr_list_data_buffer = NULL;
if (!prv_deserialize_attribute_list(serialized_slice, &attr_list, &attr_list_data_buffer)) {
PBL_LOG(LOG_LEVEL_WARNING,
"Failed to deserialize an AttributeList from a serialized slice");
return false;
}
// Check if the AttributeList has the attributes required for the slice
const bool is_attr_list_valid = prv_is_slice_attribute_list_valid(serialized_slice->type,
&attr_list);
if (!is_attr_list_valid) {
PBL_LOG(LOG_LEVEL_WARNING, "Serialized slice AttributeList is invalid");
}
attribute_list_destroy_list(&attr_list);
kernel_free(attr_list_data_buffer);
return is_attr_list_valid;
}
typedef struct SliceValidationIteratorContext {
bool is_at_least_one_slice_invalid;
size_t validated_size;
} SliceValidationIteratorContext;
//! If any slices are invalid, context.is_at_least_one_slice_invalid will be set to true.
//! If all the slices are valid, context.validated_size will hold the size of the entire serialized
//! glance after trimming any slices that go beyond the APP_GLANCE_DB_MAX_SLICES_PER_GLANCE limit.
//! @note This assumes that context.validated_size has been initialized to take into account
//! the serialized app glance's header.
static bool prv_validate_slice(SerializedAppGlanceSliceHeader *serialized_slice, void *context) {
SliceValidationIteratorContext *validation_context = context;
PBL_ASSERTN(validation_context);
if (!prv_is_serialized_slice_valid(serialized_slice)) {
*validation_context = (SliceValidationIteratorContext) {
.is_at_least_one_slice_invalid = true,
.validated_size = 0,
};
return false;
}
validation_context->validated_size += serialized_slice->total_size;
return true;
}
/////////////////////////
// AppGlanceDB API
/////////////////////////
status_t app_glance_db_insert_glance(const Uuid *uuid, const AppGlance *glance) {
if (!uuid || !glance) {
return E_INVALID_ARGUMENT;
}
SerializedAppGlanceHeader *serialized_glance = NULL;
size_t serialized_glance_size = 0;
status_t rv = prv_serialize_glance(glance, &serialized_glance, &serialized_glance_size);
if (rv == S_SUCCESS) {
rv = app_glance_db_insert((uint8_t *)uuid, UUID_SIZE, (uint8_t *)serialized_glance,
serialized_glance_size);
}
kernel_free(serialized_glance);
return rv;
}
status_t app_glance_db_read_glance(const Uuid *uuid, AppGlance *glance_out) {
if (!uuid || !glance_out) {
return E_INVALID_ARGUMENT;
}
const uint8_t *key = (uint8_t *)uuid;
const int key_size = UUID_SIZE;
const int serialized_glance_size = app_glance_db_get_len(key, key_size);
if (!serialized_glance_size) {
return E_DOES_NOT_EXIST;
} else if (serialized_glance_size < 0) {
WTF;
}
uint8_t *serialized_glance = kernel_zalloc((size_t)serialized_glance_size);
if (!serialized_glance) {
return E_OUT_OF_MEMORY;
}
status_t rv = app_glance_db_read(key, key_size, serialized_glance, serialized_glance_size);
if (rv != S_SUCCESS) {
goto cleanup;
}
rv = prv_deserialize_glance((SerializedAppGlanceHeader *)serialized_glance,
(size_t)serialized_glance_size, glance_out);
cleanup:
kernel_free(serialized_glance);
return rv;
}
status_t app_glance_db_read_creation_time(const Uuid *uuid, time_t *time_out) {
if (!uuid || !time_out) {
return E_INVALID_ARGUMENT;
}
SerializedAppGlanceHeader serialized_glance_header = {};
const status_t rv = app_glance_db_read((uint8_t *)uuid, UUID_SIZE,
(uint8_t *)&serialized_glance_header,
sizeof(serialized_glance_header));
if (rv == S_SUCCESS) {
*time_out = serialized_glance_header.creation_time;
}
return rv;
}
status_t app_glance_db_delete_glance(const Uuid *uuid) {
return app_glance_db_delete((uint8_t *)uuid, UUID_SIZE);
}
//////////////////////
// Settings helpers
//////////////////////
// TODO PBL-38080: Extract out settings file opening/closing and mutex locking/unlocking for BlobDB
static status_t prv_lock_mutex_and_open_file(void) {
mutex_lock(s_app_glance_db.mutex);
const status_t rv = settings_file_open(&s_app_glance_db.settings_file, SETTINGS_FILE_NAME,
SETTINGS_FILE_SIZE);
if (rv != S_SUCCESS) {
mutex_unlock(s_app_glance_db.mutex);
}
return rv;
}
static void prv_close_file_and_unlock_mutex(void) {
settings_file_close(&s_app_glance_db.settings_file);
mutex_unlock(s_app_glance_db.mutex);
}
/////////////////////////
// Blob DB API
/////////////////////////
void app_glance_db_init(void) {
s_app_glance_db.mutex = mutex_create();
}
status_t app_glance_db_flush(void) {
mutex_lock(s_app_glance_db.mutex);
pfs_remove(SETTINGS_FILE_NAME);
mutex_unlock(s_app_glance_db.mutex);
return S_SUCCESS;
}
static status_t prv_validate_glance(const Uuid *app_uuid,
const SerializedAppGlanceHeader *serialized_glance, size_t *len) {
// Change this block if we support multiple app glance versions in the future
// For now report an error if the glance's version isn't the current database version
if (serialized_glance->version != APP_GLANCE_DB_CURRENT_VERSION) {
PBL_LOG(LOG_LEVEL_WARNING,
"Tried to insert AppGlanceDB entry with invalid version!"
" Entry version: %"PRIu8", AppGlanceDB version: %u",
serialized_glance->version, APP_GLANCE_DB_CURRENT_VERSION);
return E_INVALID_ARGUMENT;
}
// Check that the creation_time of this new glance value is newer than any existing glance value
SerializedAppGlanceHeader existing_glance = {};
status_t rv = app_glance_db_read((uint8_t *)app_uuid, UUID_SIZE, (uint8_t *)&existing_glance,
sizeof(existing_glance));
if ((rv == S_SUCCESS) && (serialized_glance->creation_time <= existing_glance.creation_time)) {
PBL_LOG(LOG_LEVEL_WARNING,
"Tried to insert AppGlanceDB entry with older creation_time (%"PRIu32")"
" than existing entry (%"PRIu32")", serialized_glance->creation_time,
existing_glance.creation_time);
return E_INVALID_ARGUMENT;
}
// Validate the slices (which also records a `validated_size` we'll use to trim excess slices)
SliceValidationIteratorContext validation_context = {
// Start by taking into account the header of the serialized glance
.validated_size = sizeof(SerializedAppGlanceHeader),
};
// Iteration will fail if the slices report `total_size` values that
const bool iteration_succeeded =
prv_slice_for_each((SerializedAppGlanceHeader *)serialized_glance, *len,
prv_validate_slice, &validation_context);
if (!iteration_succeeded) {
PBL_LOG(LOG_LEVEL_WARNING,
"Tried to insert AppGlanceDB entry but failed to iterate over the serialized slices");
return E_INVALID_ARGUMENT;
} else if (validation_context.is_at_least_one_slice_invalid) {
PBL_LOG(LOG_LEVEL_WARNING,
"Tried to insert AppGlanceDB entry with at least one invalid slice");
return E_INVALID_ARGUMENT;
}
// Trim the serialized glance of excess slices by shrinking `val_len` to `validated_size`
// We do this if the glance entry has more slices than the max number of slices per glance.
// This can happen for glance entries sent to us by the mobile apps because they don't have a way
// of knowing the max number of slices supported by the firmware, and so they send us as many
// slices as they can fit in a BlobDB packet. We just take as many slices as we support and trim
// the excess.
if (validation_context.validated_size < *len) {
PBL_LOG(LOG_LEVEL_WARNING,
"Trimming AppGlanceDB entry of excess slices before insertion");
*len = validation_context.validated_size;
}
return S_SUCCESS;
}
status_t app_glance_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
if ((key_len != UUID_SIZE) || (val_len < (int)sizeof(SerializedAppGlanceHeader))) {
return E_INVALID_ARGUMENT;
}
const Uuid *app_uuid = (const Uuid *)key;
const SerializedAppGlanceHeader *serialized_glance = (const SerializedAppGlanceHeader *)val;
size_t len = val_len;
status_t rv = prv_validate_glance(app_uuid, serialized_glance, &len);
if (rv != S_SUCCESS) {
return rv;
}
// Fetch app if it's in the app DB, but not cached. If it's not in the app db and not a system app
// reject the glance insert
AppInstallId app_id = app_install_get_id_for_uuid(app_uuid);
if (app_install_id_from_app_db(app_id)) {
// Bump the app's priority by telling the cache we're using it
if (app_cache_entry_exists(app_id)) {
app_cache_app_launched(app_id);
} else {
// The app isn't cached. Fetch it!
PebbleEvent e = {
.type = PEBBLE_APP_FETCH_REQUEST_EVENT,
.app_fetch_request = {
.id = app_id,
.with_ui = false,
.fetch_args = NULL,
},
};
event_put(&e);
}
} else if (!app_install_id_from_system(app_id)) {
// App is not installed (not in app db and not a system app). Do not insert the glance
// String initialized on the heap to reduce stack usage
char *app_uuid_string = kernel_malloc_check(UUID_STRING_BUFFER_LENGTH);
uuid_to_string(app_uuid, app_uuid_string);
PBL_LOG(LOG_LEVEL_WARNING,
"Attempted app glance insert for an app that's not installed. UUID: %s",
app_uuid_string);
kernel_free(app_uuid_string);
return E_DOES_NOT_EXIST;
}
rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_set(&s_app_glance_db.settings_file, key, (size_t)key_len, serialized_glance,
len);
prv_close_file_and_unlock_mutex();
return rv;
}
int app_glance_db_get_len(const uint8_t *key, int key_len) {
if (key_len != UUID_SIZE) {
return 0;
}
const status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return 0;
}
const int length = settings_file_get_len(&s_app_glance_db.settings_file, key, (size_t)key_len);
prv_close_file_and_unlock_mutex();
return length;
}
status_t app_glance_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len) {
if ((key_len != UUID_SIZE) || val_out == NULL) {
return E_INVALID_ARGUMENT;
}
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_get(&s_app_glance_db.settings_file, key, (size_t)key_len, val_out,
(size_t)val_out_len);
if (rv == S_SUCCESS) {
SerializedAppGlanceHeader *serialized_app_glance = (SerializedAppGlanceHeader *)val_out;
// Change this block if we support multiple app glance versions in the future
if (serialized_app_glance->version != APP_GLANCE_DB_CURRENT_VERSION) {
// Clear out the stale entry
PBL_LOG(LOG_LEVEL_WARNING, "Read a AppGlanceDB entry with an outdated version; deleting it");
settings_file_delete(&s_app_glance_db.settings_file, key, (size_t)key_len);
rv = E_DOES_NOT_EXIST;
}
}
prv_close_file_and_unlock_mutex();
return rv;
}
status_t app_glance_db_delete(const uint8_t *key, int key_len) {
if (key_len != UUID_SIZE) {
return E_INVALID_ARGUMENT;
}
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
if (settings_file_exists(&s_app_glance_db.settings_file, key, (size_t)key_len)) {
rv = settings_file_delete(&s_app_glance_db.settings_file, key, (size_t)key_len);
} else {
rv = S_SUCCESS;
}
prv_close_file_and_unlock_mutex();
return rv;
}
/////////////////////////
// Testing code
/////////////////////////
#if UNITTEST
void app_glance_db_deinit(void) {
app_glance_db_flush();
mutex_destroy(s_app_glance_db.mutex);
}
status_t app_glance_db_insert_stale(const uint8_t *key, int key_len, const uint8_t *val,
int val_len) {
// Quick and dirty insert which doesn't do any error checking. Used to insert stale entries
// for testing
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_set(&s_app_glance_db.settings_file, key, key_len, val, val_len);
prv_close_file_and_unlock_mutex();
return rv;
}
#endif

View file

@ -0,0 +1,51 @@
/*
* 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/normal/app_glances/app_glance_service.h"
#include "system/status_codes.h"
#include "util/attributes.h"
#include "util/time/time.h"
#include "util/uuid.h"
#include <stdint.h>
// -------------------------------------------------------------------------------------------------
// AppGlanceDB Implementation
status_t app_glance_db_insert_glance(const Uuid *uuid, const AppGlance *glance);
status_t app_glance_db_read_glance(const Uuid *uuid, AppGlance *glance_out);
status_t app_glance_db_read_creation_time(const Uuid *uuid, time_t *time_out);
status_t app_glance_db_delete_glance(const Uuid *uuid);
// -------------------------------------------------------------------------------------------------
// BlobDB API Implementation
void app_glance_db_init(void);
status_t app_glance_db_flush(void);
status_t app_glance_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
int app_glance_db_get_len(const uint8_t *key, int key_len);
status_t app_glance_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
status_t app_glance_db_delete(const uint8_t *key, int key_len);

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 "services/normal/timeline/attribute.h"
#include "services/normal/timeline/attribute_private.h"
#include "util/attributes.h"
#define APP_GLANCE_DB_CURRENT_VERSION (1)
//! This number is reduced for unit tests to avoid creating large glance payloads in the unit tests
#if UNITTEST
#define APP_GLANCE_DB_MAX_SLICES_PER_GLANCE (2)
#else
#define APP_GLANCE_DB_MAX_SLICES_PER_GLANCE (8)
#endif
#define APP_GLANCE_DB_MAX_NUM_APP_GLANCES (50)
typedef struct PACKED SerializedAppGlanceHeader {
uint8_t version;
uint32_t creation_time;
uint8_t data[]; // Serialized slices
} SerializedAppGlanceHeader;
typedef struct PACKED SerializedAppGlanceSliceHeader {
uint16_t total_size;
uint8_t type;
uint8_t num_attributes;
uint8_t data[]; // Serialized attributes
} SerializedAppGlanceSliceHeader;
//! The minimum size of an AppGlanceSliceType_IconAndSubtitle slice is the size of the header plus
//! the expiration_time because the icon and subtitle are optional
#define APP_GLANCE_DB_ICON_AND_SUBTITLE_SLICE_MIN_SIZE \
(sizeof(SerializedAppGlanceSliceHeader) + sizeof(SerializedAttributeHeader) + sizeof(uint32_t))
//! The maximum size of an AppGlanceSliceType_IconAndSubtitle slice is the size of the header plus
//! the expiration_time, icon resource ID, and subtitle string attributes (+1 added for null char)
#define APP_GLANCE_DB_ICON_AND_SUBTITLE_SLICE_MAX_SIZE \
(sizeof(SerializedAppGlanceSliceHeader) + (sizeof(SerializedAttributeHeader) * 3) + \
sizeof(uint32_t) + sizeof(uint32_t) + ATTRIBUTE_APP_GLANCE_SUBTITLE_MAX_LEN + 1)
#define APP_GLANCE_DB_SLICE_MIN_SIZE (APP_GLANCE_DB_ICON_AND_SUBTITLE_SLICE_MIN_SIZE)
#define APP_GLANCE_DB_SLICE_MAX_SIZE (APP_GLANCE_DB_ICON_AND_SUBTITLE_SLICE_MAX_SIZE)

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.
*/
#include "contacts_db.h"
#include "kernel/pbl_malloc.h"
#include "services/normal/filesystem/pfs.h"
#include "services/normal/settings/settings_file.h"
#include "services/normal/contacts/attributes_address.h"
#include "services/normal/contacts/contacts.h"
#include "os/mutex.h"
#include "system/passert.h"
#include "system/status_codes.h"
#include "util/units.h"
#include "util/uuid.h"
#define SETTINGS_FILE_NAME "contactsdb"
#define SETTINGS_FILE_SIZE (KiBYTES(30))
static struct {
SettingsFile settings_file;
PebbleMutex *mutex;
} s_contacts_db;
//////////////////////
// Settings helpers
//////////////////////
static status_t prv_lock_mutex_and_open_file(void) {
mutex_lock(s_contacts_db.mutex);
status_t rv = settings_file_open(&s_contacts_db.settings_file,
SETTINGS_FILE_NAME,
SETTINGS_FILE_SIZE);
if (rv != S_SUCCESS) {
mutex_unlock(s_contacts_db.mutex);
}
return rv;
}
static void prv_close_file_and_unlock_mutex(void) {
settings_file_close(&s_contacts_db.settings_file);
mutex_unlock(s_contacts_db.mutex);
}
//////////////////////////////
// Contacts DB API
//////////////////////////////
int contacts_db_get_serialized_contact(const Uuid *uuid, SerializedContact **contact_out) {
*contact_out = NULL;
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return 0;
}
const unsigned contact_len = settings_file_get_len(&s_contacts_db.settings_file,
(uint8_t *)uuid, UUID_SIZE);
if (contact_len < sizeof(SerializedContact)) {
prv_close_file_and_unlock_mutex();
return 0;
}
*contact_out = task_zalloc(contact_len);
if (!*contact_out) {
prv_close_file_and_unlock_mutex();
return 0;
}
rv = settings_file_get(&s_contacts_db.settings_file, (uint8_t *)uuid, UUID_SIZE,
(void *) *contact_out, contact_len);
prv_close_file_and_unlock_mutex();
if (rv != S_SUCCESS) {
task_free(*contact_out);
return 0;
}
SerializedContact *serialized_contact = (SerializedContact *)*contact_out;
return (contact_len - sizeof(SerializedContact));
}
void contacts_db_free_serialized_contact(SerializedContact *contact) {
task_free(contact);
}
/////////////////////////
// Blob DB API
/////////////////////////
void contacts_db_init(void) {
memset(&s_contacts_db, 0, sizeof(s_contacts_db));
s_contacts_db.mutex = mutex_create();
}
status_t contacts_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
if (key_len != UUID_SIZE || val_len < (int) sizeof(SerializedContact)) {
return E_INVALID_ARGUMENT;
}
// TODO: Verify the serialized_contact data before storing it
SerializedContact *serialized_contact = (SerializedContact *)val;
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_set(&s_contacts_db.settings_file, key, key_len, val, val_len);
prv_close_file_and_unlock_mutex();
return rv;
}
int contacts_db_get_len(const uint8_t *key, int key_len) {
if (key_len != UUID_SIZE) {
return 0;
}
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_get_len(&s_contacts_db.settings_file, key, key_len);
prv_close_file_and_unlock_mutex();
return rv;
}
status_t contacts_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len) {
if (key_len != UUID_SIZE || val_out == NULL) {
return E_INVALID_ARGUMENT;
}
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_get(&s_contacts_db.settings_file, key, key_len, val_out, val_out_len);
prv_close_file_and_unlock_mutex();
SerializedContact *serialized_contact = (SerializedContact *)val_out;
return rv;
}
status_t contacts_db_delete(const uint8_t *key, int key_len) {
if (key_len != UUID_SIZE) {
return E_INVALID_ARGUMENT;
}
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_delete(&s_contacts_db.settings_file, key, key_len);
prv_close_file_and_unlock_mutex();
return rv;
}
status_t contacts_db_flush(void) {
mutex_lock(s_contacts_db.mutex);
status_t rv = pfs_remove(SETTINGS_FILE_NAME);
mutex_unlock(s_contacts_db.mutex);
return rv;
}

View file

@ -0,0 +1,57 @@
/*
* 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 "system/status_codes.h"
#include "util/attributes.h"
#include "util/uuid.h"
typedef struct PACKED {
Uuid uuid;
uint32_t flags;
uint8_t num_attributes;
uint8_t num_addresses;
uint8_t data[]; // Serialized attributes followed by serialized addresses
} SerializedContact;
//! Given a contact's uuid, return the serialized data for that contact. This should probably only
//! be called by the contacts service. You probably want contacts_get_contact_by_uuid() instead
//! @param uuid The contact's uuid.
//! @param contact_out A pointer to the serialized contact data, NULL if the contact isn't found.
//! @return The length of the data[] field.
//! @note The caller must cleanup with contacts_db_free_serialized_contact().
int contacts_db_get_serialized_contact(const Uuid *uuid, SerializedContact **contact_out);
//! Frees the serialized contact data returned by contacts_db_get_serialized_contact().
void contacts_db_free_serialized_contact(SerializedContact *contact);
///////////////////////////////////////////
// BlobDB Boilerplate (see blob_db/api.h)
///////////////////////////////////////////
void contacts_db_init(void);
status_t contacts_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
int contacts_db_get_len(const uint8_t *key, int key_len);
status_t contacts_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
status_t contacts_db_delete(const uint8_t *key, int key_len);
status_t contacts_db_flush(void);

View file

@ -0,0 +1,299 @@
/*
* 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 "sync.h"
#include "endpoint_private.h"
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
#include "services/common/comm_session/session.h"
#include "services/common/analytics/analytics.h"
#include "system/logging.h"
#include "system/passert.h"
#include "system/status_codes.h"
#include "system/hexdump.h"
#include "util/attributes.h"
#include "util/net.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
//! @file endpoint.c
//! BlobDB Endpoint
//!
//! There are 3 commands implemented in this endpoint: INSERT, DELETE, and CLEAR
//!
//! <b>INSERT:</b> This command will insert a key and value into the database specified.
//!
//! \code{.c}
//! 0x01 <uint16_t token> <uint8_t DatabaseId>
//! <uint8_t key_size M> <uint8_t[M]> key_bytes>
//! <uint16_t value_size N> <uint8_t[N]> value_bytes>
//! \endcode
//!
//! <b>DELETE:</b> This command will delete an entry with the key in the database specified.
//!
//! \code{.c}
//! 0x04 <uint16_t token> <uint8_t DatabaseId>
//! <uint8_t key_size M> <uint8_t[M]> key_bytes>
//! \endcode
//!
//! <b>CLEAR:</b> This command will clear all entries in the database specified.
//!
//! \code{.c}
//! 0x05 <uint16_t token> <uint8_t DatabaseId>
//! \endcode
//! BlobDB Endpoint ID
static const uint16_t BLOB_DB_ENDPOINT_ID = 0xb1db;
static const uint8_t KEY_DATA_LENGTH = (sizeof(uint8_t) + sizeof(uint8_t));
static const uint8_t VALUE_DATA_LENGTH = (sizeof(uint16_t) + sizeof(uint8_t));
//! Message Length Constants
static const uint8_t MIN_INSERT_LENGTH = 8;
static const uint8_t MIN_DELETE_LENGTH = 6;
static const uint8_t MIN_CLEAR_LENGTH = 3;
static bool s_bdb_accepting_messages;
static void prv_send_response(CommSession *session, BlobDBToken token, BlobDBResponse result) {
struct PACKED BlobDBResponseMsg {
BlobDBToken token;
BlobDBResponse result;
} response = {
.token = token,
.result = result,
};
comm_session_send_data(session, BLOB_DB_ENDPOINT_ID, (uint8_t*)&response, sizeof(response),
COMM_SESSION_DEFAULT_TIMEOUT);
}
static BlobDBResponse prv_interpret_db_ret_val(status_t ret_val) {
switch (ret_val) {
case S_SUCCESS:
return BLOB_DB_SUCCESS;
case E_DOES_NOT_EXIST:
return BLOB_DB_KEY_DOES_NOT_EXIST;
case E_RANGE:
return BLOB_DB_INVALID_DATABASE_ID;
case E_INVALID_ARGUMENT:
return BLOB_DB_INVALID_DATA;
case E_OUT_OF_STORAGE:
return BLOB_DB_DATABASE_FULL;
case E_INVALID_OPERATION:
return BLOB_DB_DATA_STALE;
default:
PBL_LOG(LOG_LEVEL_WARNING, "BlobDB return value caught by default case");
return BLOB_DB_GENERAL_FAILURE;
}
}
static const uint8_t *prv_read_ptr(const uint8_t *iter, const uint8_t *iter_end,
const uint8_t **out_buf, uint16_t buf_len) {
// >= because we will be reading more bytes after this point
if ((buf_len == 0) || ((iter + buf_len) > iter_end)) {
PBL_LOG(LOG_LEVEL_WARNING, "BlobDB: read invalid length");
return NULL;
}
// grab pointer to start of buf
*out_buf = (uint8_t*)iter;
iter += buf_len;
return iter;
}
static const uint8_t *prv_read_key_size(const uint8_t *iter, const uint8_t *iter_end,
uint8_t *out_int) {
// copy length from iter to out_len, then save a local copy of the length
*out_int = *iter++;
return iter;
}
static const uint8_t *prv_read_value_size(const uint8_t *iter, const uint8_t *iter_end,
uint16_t *out_int) {
// copy length from iter to out_len, then save a local copy of the length
*out_int = *(uint16_t*)iter;
iter += sizeof(uint16_t);
return iter;
}
static BlobDBToken prv_try_read_token(const uint8_t *data, uint32_t length) {
if (length < sizeof(BlobDBToken)) {
return 0;
}
return *(BlobDBToken*)data;
}
static void prv_handle_database_insert(CommSession *session, const uint8_t *data, uint32_t length) {
if (length < MIN_INSERT_LENGTH) {
prv_send_response(session, prv_try_read_token(data, length), BLOB_DB_INVALID_DATA);
return;
}
const uint8_t *iter = data;
BlobDBToken token;
BlobDBId db_id;
// Read token and db_id
iter = endpoint_private_read_token_db_id(iter, &token, &db_id);
// read key length and key bytes ptr
uint8_t key_size;
const uint8_t *key_bytes = NULL;
iter = prv_read_key_size(iter, data + length, &key_size);
iter = prv_read_ptr(iter, data + length, &key_bytes, key_size);
// If read past end or there is not enough data left in buffer for a value size and data to exist
if (!iter || (iter > (data + length - VALUE_DATA_LENGTH))) {
prv_send_response(session, token, BLOB_DB_INVALID_DATA);
return;
}
// read value length and value bytes ptr
uint16_t value_size;
const uint8_t *value_bytes = NULL;
iter = prv_read_value_size(iter, data + length, &value_size);
iter = prv_read_ptr(iter, data + length, &value_bytes, value_size);
// If we read too many bytes or didn't read all the bytes (2nd test)
if (!iter || (iter != (data + length))) {
prv_send_response(session, token, BLOB_DB_INVALID_DATA);
return;
}
// perform action on database and return result
status_t ret = blob_db_insert(db_id, key_bytes, key_size, value_bytes, value_size);
prv_send_response(session, token, prv_interpret_db_ret_val(ret));
}
static void prv_handle_database_delete(CommSession *session, const uint8_t *data, uint32_t length) {
if (length < MIN_DELETE_LENGTH) {
prv_send_response(session, prv_try_read_token(data, length), BLOB_DB_INVALID_DATA);
return;
}
const uint8_t *iter = data;
BlobDBToken token;
BlobDBId db_id;
// Read token and db_id
iter = endpoint_private_read_token_db_id(iter, &token, &db_id);
// Read key length and key bytes
uint8_t key_size;
const uint8_t *key_bytes = NULL;
iter = prv_read_key_size(iter, data + length, &key_size);
iter = prv_read_ptr(iter, data + length, &key_bytes, key_size);
// If we read too many bytes or key_size is 0 or didn't read all the bytes
if (!iter || (iter != (data + length))) {
prv_send_response(session, token, BLOB_DB_INVALID_DATA);
return;
}
// perform action on database and return result
status_t ret = blob_db_delete(db_id, key_bytes, key_size);
prv_send_response(session, token, prv_interpret_db_ret_val(ret));
}
static void prv_handle_database_clear(CommSession *session, const uint8_t *data, uint32_t length) {
if (length < MIN_CLEAR_LENGTH) {
prv_send_response(session, prv_try_read_token(data, length), BLOB_DB_INVALID_DATA);
return;
}
BlobDBToken token;
BlobDBId db_id;
// Read token and db_id
endpoint_private_read_token_db_id(data, &token, &db_id);
// perform action on database and return result
status_t ret = blob_db_flush(db_id);
prv_send_response(session, token, prv_interpret_db_ret_val(ret));
// Mark the device as faithful after successfully flushing
if (ret == S_SUCCESS) {
bt_persistent_storage_set_unfaithful(false /* We are now faithful */);
}
}
static void prv_blob_db_msg_decode_and_handle(
CommSession *session, BlobDBCommand cmd, const uint8_t *data, size_t data_length) {
switch (cmd) {
case BLOB_DB_COMMAND_INSERT:
PBL_LOG(LOG_LEVEL_DEBUG, "Got INSERT");
prv_handle_database_insert(session, data, data_length);
break;
case BLOB_DB_COMMAND_DELETE:
PBL_LOG(LOG_LEVEL_DEBUG, "Got DELETE");
prv_handle_database_delete(session, data, data_length);
break;
case BLOB_DB_COMMAND_CLEAR:
PBL_LOG(LOG_LEVEL_DEBUG, "Got CLEAR");
prv_handle_database_clear(session, data, data_length);
break;
// Commands not implemented.
case BLOB_DB_COMMAND_READ:
case BLOB_DB_COMMAND_UPDATE:
PBL_LOG(LOG_LEVEL_ERROR, "BlobDB Command not implemented");
// Fallthrough
default:
PBL_LOG(LOG_LEVEL_ERROR, "Invalid BlobDB message received, cmd is %u", cmd);
prv_send_response(session, prv_try_read_token(data, data_length), BLOB_DB_INVALID_OPERATION);
break;
}
}
void blob_db_protocol_msg_callback(CommSession *session, const uint8_t* data, size_t length) {
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
analytics_inc(ANALYTICS_DEVICE_METRIC_BLOB_DB_EVENT_COUNT, AnalyticsClient_System);
PBL_HEXDUMP_D(LOG_DOMAIN_BLOBDB, LOG_LEVEL_DEBUG, data, length);
// Each BlobDB message is required to have at least a Command and a Token
static const uint8_t MIN_RAW_DATA_LEN = sizeof(BlobDBCommand) + sizeof(BlobDBToken);
if (length < MIN_RAW_DATA_LEN) {
PBL_LOG(LOG_LEVEL_ERROR, "Got a blob_db message that was too short, len: %zu", length);
prv_send_response(session, 0, BLOB_DB_INVALID_DATA);
return;
}
const BlobDBCommand cmd = *data;
data += sizeof(BlobDBCommand); // fwd to message contents
const size_t data_length = length - sizeof(BlobDBCommand);
if (!s_bdb_accepting_messages) {
prv_send_response(session, prv_try_read_token(data, length), BLOB_DB_TRY_LATER);
return;
}
prv_blob_db_msg_decode_and_handle(session, cmd, data, data_length);
}
void blob_db_set_accepting_messages(bool enabled) {
s_bdb_accepting_messages = enabled;
}

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 "endpoint_private.h"
//! Send a write message for the given blob db item.
//! @returns the blob db transaction token
BlobDBToken blob_db_endpoint_send_write(BlobDBId db_id,
time_t last_updated,
const void *key,
int key_len,
const void *val,
int val_len);
//! Send a WB message for the given blob db item.
//! @returns the blob db transaction token
BlobDBToken blob_db_endpoint_send_writeback(BlobDBId db_id,
time_t last_updated,
const void *key,
int key_len,
const void *val,
int val_len);
//! Indicate that blob db sync is done for a given db id
void blob_db_endpoint_send_sync_done(BlobDBId db_id);

View file

@ -0,0 +1,354 @@
/*
* 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 "api.h"
#include "sync.h"
#include "endpoint_private.h"
#include "services/common/comm_session/session.h"
#include "services/common/comm_session/session_send_buffer.h"
#include "services/common/analytics/analytics.h"
#include "system/logging.h"
#include "system/passert.h"
#include "system/status_codes.h"
#include "system/hexdump.h"
#include "util/attributes.h"
#include "util/net.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
//! BlobDB Endpoint ID
static const uint16_t BLOB_DB2_ENDPOINT_ID = 0xb2db;
//! Message Length Constants
static const uint8_t DIRTY_DATABASES_LENGTH = 2;
static const uint8_t START_SYNC_LENGTH = 3;
static const uint8_t WRITE_RESPONSE_LENGTH = 3;
static const uint8_t WRITEBACK_RESPONSE_LENGTH = 3;
static const uint8_t SYNC_DONE_RESPONSE_LENGTH = 3;
static bool s_b2db_accepting_messages;
T_STATIC BlobDBToken prv_new_token(void) {
static BlobDBToken next_token = 1; // 0 token should be avoided
return next_token++;
}
static const uint8_t *prv_read_token_and_response(const uint8_t *iter, BlobDBToken *out_token,
BlobDBResponse *out_response) {
*out_token = *(BlobDBToken *)iter;
iter += sizeof(BlobDBToken);
*out_response = *(BlobDBResponse *)iter;
iter += sizeof(BlobDBResponse);
return iter;
}
T_STATIC void prv_send_response(CommSession *session, uint8_t *response,
uint8_t response_length) {
comm_session_send_data(session, BLOB_DB2_ENDPOINT_ID, response, response_length,
COMM_SESSION_DEFAULT_TIMEOUT);
}
static void prv_handle_get_dirty_databases(CommSession *session,
const uint8_t *data,
uint32_t length) {
if (length < DIRTY_DATABASES_LENGTH) {
PBL_LOG(LOG_LEVEL_ERROR, "Got a dirty databases with an invalid length: %"PRIu32"", length);
return;
}
struct PACKED DirtyDatabasesResponseMsg {
BlobDBCommand cmd;
BlobDBToken token;
BlobDBResponse result;
uint8_t num_ids;
BlobDBId db_ids[NumBlobDBs];
} response = {
.cmd = BLOB_DB_COMMAND_DIRTY_DBS_RESPONSE,
.token = *(BlobDBToken *)data,
.result = BLOB_DB_SUCCESS,
};
blob_db_get_dirty_dbs(response.db_ids, &response.num_ids);
// we don't want to send the extra bytes in response.db_ids
int num_empty_ids = NumBlobDBs - response.num_ids;
prv_send_response(session, (uint8_t *) &response, sizeof(response) - num_empty_ids);
}
static void prv_handle_start_sync(CommSession *session,
const uint8_t *data,
uint32_t length) {
if (length < START_SYNC_LENGTH) {
PBL_LOG(LOG_LEVEL_ERROR, "Got a start sync with an invalid length: %"PRIu32"", length);
return;
}
struct PACKED StartSyncResponseMsg {
BlobDBCommand cmd;
BlobDBToken token;
BlobDBResponse result;
} response = {
.cmd = BLOB_DB_COMMAND_START_SYNC_RESPONSE,
};
BlobDBId db_id;
endpoint_private_read_token_db_id(data, &response.token, &db_id);
status_t rv = blob_db_sync_db(db_id);
switch (rv) {
case S_SUCCESS:
case S_NO_ACTION_REQUIRED:
response.result = BLOB_DB_SUCCESS;
break;
case E_INVALID_ARGUMENT:
response.result = BLOB_DB_INVALID_DATABASE_ID;
break;
case E_BUSY:
response.result = BLOB_DB_TRY_LATER;
break;
default:
response.result = BLOB_DB_GENERAL_FAILURE;
break;
}
prv_send_response(session, (uint8_t *)&response, sizeof(response));
}
static void prv_handle_wb_write_response(const uint8_t *data,
uint32_t length) {
// read token and response code
BlobDBToken token;
BlobDBResponse response_code;
prv_read_token_and_response(data, &token, &response_code);
BlobDBSyncSession *sync_session = blob_db_sync_get_session_for_token(token);
if (sync_session) {
if (response_code == BLOB_DB_SUCCESS) {
blob_db_sync_next(sync_session);
} else {
blob_db_sync_cancel(sync_session);
}
} else {
// No session
PBL_LOG(LOG_LEVEL_WARNING, "received blob db wb response with an invalid token: %d", token);
}
}
static void prv_handle_write_response(CommSession *session,
const uint8_t *data,
uint32_t length) {
if (length < WRITE_RESPONSE_LENGTH) {
PBL_LOG(LOG_LEVEL_ERROR, "Got a write response with an invalid length: %"PRIu32"", length);
return;
}
prv_handle_wb_write_response(data, length);
}
static void prv_handle_wb_response(CommSession *session,
const uint8_t *data,
uint32_t length) {
if (length < WRITEBACK_RESPONSE_LENGTH) {
PBL_LOG(LOG_LEVEL_ERROR, "Got a writeback response with an invalid length: %"PRIu32"", length);
return;
}
prv_handle_wb_write_response(data, length);
}
static void prv_handle_sync_done_response(CommSession *session,
const uint8_t *data,
uint32_t length) {
if (length < SYNC_DONE_RESPONSE_LENGTH) {
PBL_LOG(LOG_LEVEL_ERROR, "Got a sync done response with an invalid length: %"PRIu32"", length);
return;
}
// read token and response code
BlobDBToken token;
BlobDBResponse response_code;
prv_read_token_and_response(data, &token, &response_code);
if (response_code != BLOB_DB_SUCCESS) {
PBL_LOG(LOG_LEVEL_ERROR, "Sync Done response error: %d", response_code);
}
}
static void prv_send_error_response(CommSession *session,
BlobDBCommand cmd,
const uint8_t *data,
BlobDBResponse response_code) {
struct PACKED ErrorResponseMsg {
BlobDBCommand cmd;
BlobDBToken token;
BlobDBResponse result;
} response = {
.cmd = cmd | RESPONSE_MASK,
.token = *(BlobDBToken *)data,
.result = response_code,
};
prv_send_response(session, (uint8_t *)&response, sizeof(response));
}
static void prv_blob_db_msg_decode_and_handle(
CommSession *session, BlobDBCommand cmd, const uint8_t *data, size_t data_length) {
switch (cmd) {
case BLOB_DB_COMMAND_DIRTY_DBS:
PBL_LOG(LOG_LEVEL_DEBUG, "Got DIRTY DBs");
prv_handle_get_dirty_databases(session, data, data_length);
break;
case BLOB_DB_COMMAND_START_SYNC:
PBL_LOG(LOG_LEVEL_DEBUG, "Got SYNC");
prv_handle_start_sync(session, data, data_length);
break;
case BLOB_DB_COMMAND_WRITE_RESPONSE:
PBL_LOG(LOG_LEVEL_DEBUG, "WRITE Response");
prv_handle_write_response(session, data, data_length);
break;
case BLOB_DB_COMMAND_WRITEBACK_RESPONSE:
PBL_LOG(LOG_LEVEL_DEBUG, "WRITEBACK Response");
prv_handle_wb_response(session, data, data_length);
break;
case BLOB_DB_COMMAND_SYNC_DONE_RESPONSE:
PBL_LOG(LOG_LEVEL_DEBUG, "SYNC DONE Response");
prv_handle_sync_done_response(session, data, data_length);
break;
default:
PBL_LOG(LOG_LEVEL_ERROR, "Invalid BlobDB2 message received, cmd is %u", cmd);
prv_send_error_response(session, cmd, data, BLOB_DB_INVALID_OPERATION);
break;
}
}
static uint16_t prv_send_write_writeback(BlobDBCommand cmd,
BlobDBId db_id,
time_t last_updated,
const uint8_t *key,
int key_len,
const uint8_t *val,
int val_len) {
struct PACKED WritebackMetadata {
BlobDBCommand cmd;
BlobDBToken token;
BlobDBId db_id;
uint32_t last_updated;
} writeback_metadata = {
.cmd = cmd,
.token = prv_new_token(),
.db_id = db_id,
.last_updated = last_updated,
};
size_t writeback_length = sizeof(writeback_metadata) +
sizeof(uint8_t) /* key length size*/ +
key_len +
sizeof(uint16_t) /* val length size */ +
val_len;
SendBuffer *sb = comm_session_send_buffer_begin_write(comm_session_get_system_session(),
BLOB_DB2_ENDPOINT_ID,
writeback_length,
COMM_SESSION_DEFAULT_TIMEOUT);
if (sb) {
comm_session_send_buffer_write(sb, (uint8_t *)&writeback_metadata, sizeof(writeback_metadata));
comm_session_send_buffer_write(sb, (uint8_t *)&key_len, sizeof(uint8_t));
comm_session_send_buffer_write(sb, key, key_len);
comm_session_send_buffer_write(sb, (uint8_t *)&val_len, sizeof(uint16_t));
comm_session_send_buffer_write(sb, val, val_len);
comm_session_send_buffer_end_write(sb);
}
return writeback_metadata.token;
}
BlobDBToken blob_db_endpoint_send_write(BlobDBId db_id,
time_t last_updated,
const void *key,
int key_len,
const void *val,
int val_len) {
BlobDBToken token = prv_send_write_writeback(BLOB_DB_COMMAND_WRITE, db_id, last_updated,
key, key_len, val, val_len);
return token;
}
BlobDBToken blob_db_endpoint_send_writeback(BlobDBId db_id,
time_t last_updated,
const void *key,
int key_len,
const void *val,
int val_len) {
BlobDBToken token = prv_send_write_writeback(BLOB_DB_COMMAND_WRITEBACK, db_id, last_updated,
key, key_len, val, val_len);
return token;
}
void blob_db_endpoint_send_sync_done(BlobDBId db_id) {
struct PACKED SyncDoneMsg {
BlobDBCommand cmd;
BlobDBToken token;
BlobDBId db_id;
} msg = {
.cmd = BLOB_DB_COMMAND_SYNC_DONE,
.token = prv_new_token(),
.db_id = db_id,
};
PBL_LOG(LOG_LEVEL_DEBUG, "Sending sync done for db: %d", db_id);
comm_session_send_data(comm_session_get_system_session(),
BLOB_DB2_ENDPOINT_ID,
(uint8_t *)&msg,
sizeof(msg),
COMM_SESSION_DEFAULT_TIMEOUT);
}
void blob_db2_protocol_msg_callback(CommSession *session, const uint8_t* data, size_t length) {
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
analytics_inc(ANALYTICS_DEVICE_METRIC_BLOB_DB_EVENT_COUNT, AnalyticsClient_System);
// Each BlobDB message is required to have at least a Command and a Token
static const uint8_t MIN_RAW_DATA_LEN = sizeof(BlobDBCommand) + sizeof(BlobDBToken);
if (length < MIN_RAW_DATA_LEN) {
// We don't send failure responses for too short messages in endpoint2
PBL_LOG(LOG_LEVEL_ERROR, "Got a blob_db2 message that was too short, len: %zu", length);
return;
}
const BlobDBCommand cmd = *data;
data += sizeof(BlobDBCommand); // fwd to message contents
const size_t data_length = length - sizeof(BlobDBCommand);
if (!s_b2db_accepting_messages) {
prv_send_error_response(session, cmd, data, BLOB_DB_TRY_LATER);
return;
}
prv_blob_db_msg_decode_and_handle(session, cmd, data, data_length);
}
void blob_db2_set_accepting_messages(bool enabled) {
s_b2db_accepting_messages = enabled;
}

View file

@ -0,0 +1,39 @@
/*
* 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 "endpoint_private.h"
#include <stdbool.h>
extern void blob_db_set_accepting_messages(bool enabled);
extern void blob_db2_set_accepting_messages(bool enabled);
void blob_db_enabled(bool enabled) {
blob_db_set_accepting_messages(enabled);
blob_db2_set_accepting_messages(enabled);
}
const uint8_t *endpoint_private_read_token_db_id(const uint8_t *iter, BlobDBToken *out_token,
BlobDBId *out_db_id) {
// read token
*out_token = *(BlobDBToken*)iter;
iter += sizeof(BlobDBToken);
// read database id
*out_db_id = *iter;
iter += sizeof(BlobDBId);
return iter;
}

View file

@ -0,0 +1,72 @@
/*
* 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 "api.h"
#include <stdbool.h>
#include <stdint.h>
#include "util/attributes.h"
typedef uint16_t BlobDBToken;
//! Response / result values
typedef enum PACKED {
BLOB_DB_SUCCESS = 0x01,
BLOB_DB_GENERAL_FAILURE = 0x02,
BLOB_DB_INVALID_OPERATION = 0x03,
BLOB_DB_INVALID_DATABASE_ID = 0x04,
BLOB_DB_INVALID_DATA = 0x05,
BLOB_DB_KEY_DOES_NOT_EXIST = 0x06,
BLOB_DB_DATABASE_FULL = 0x07,
BLOB_DB_DATA_STALE = 0x08,
BLOB_DB_DB_NOT_SUPPORTED = 0x09,
BLOB_DB_DB_LOCKED = 0x0A,
BLOB_DB_TRY_LATER = 0x0B,
} BlobDBResponse;
_Static_assert(sizeof(BlobDBResponse) == 1, "BlobDBResponse is larger than 1 byte");
#define RESPONSE_MASK (1 << 7)
typedef enum PACKED {
BLOB_DB_COMMAND_INSERT = 0x01,
BLOB_DB_COMMAND_READ = 0x02, // Not implemented yet
BLOB_DB_COMMAND_UPDATE = 0x03, // Not implemented yet
BLOB_DB_COMMAND_DELETE = 0x04,
BLOB_DB_COMMAND_CLEAR = 0x05,
// Commands below were added as part of sync and may not all be supported by the phone
BLOB_DB_COMMAND_DIRTY_DBS = 0x06,
BLOB_DB_COMMAND_START_SYNC = 0x07,
BLOB_DB_COMMAND_WRITE = 0x08,
BLOB_DB_COMMAND_WRITEBACK = 0x09,
BLOB_DB_COMMAND_SYNC_DONE = 0x0A,
// Response commands
BLOB_DB_COMMAND_DIRTY_DBS_RESPONSE = BLOB_DB_COMMAND_DIRTY_DBS | RESPONSE_MASK,
BLOB_DB_COMMAND_START_SYNC_RESPONSE = BLOB_DB_COMMAND_START_SYNC | RESPONSE_MASK,
BLOB_DB_COMMAND_WRITE_RESPONSE = BLOB_DB_COMMAND_WRITE | RESPONSE_MASK,
BLOB_DB_COMMAND_WRITEBACK_RESPONSE = BLOB_DB_COMMAND_WRITEBACK | RESPONSE_MASK,
BLOB_DB_COMMAND_SYNC_DONE_RESPONSE = BLOB_DB_COMMAND_SYNC_DONE | RESPONSE_MASK,
} BlobDBCommand;
_Static_assert(sizeof(BlobDBCommand) == 1, "BlobDBCommand is larger than 1 byte");
const uint8_t *endpoint_private_read_token_db_id(const uint8_t *iter, BlobDBToken *out_token,
BlobDBId *out_db_id);
void blob_db_enabled(bool enabled);

View file

@ -0,0 +1,437 @@
/*
* 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 "health_db.h"
#include "console/prompt.h"
#include "kernel/pbl_malloc.h"
#include "os/mutex.h"
#include "services/normal/activity/activity_private.h"
#include "services/normal/activity/hr_util.h"
#include "services/normal/blob_db/api.h"
#include "services/normal/filesystem/pfs.h"
#include "services/normal/settings/settings_file.h"
#include "system/hexdump.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/attributes.h"
#include "util/units.h"
#include <stdio.h>
#include <string.h>
#define HEALTH_DB_DEBUG 0
#define HEALTH_DB_MAX_KEY_LEN 30
static const char *HEALTH_DB_FILE_NAME = "healthdb";
static const int HEALTH_DB_MAX_SIZE = KiBYTES(12);
static PebbleMutex *s_mutex;
#define MOVEMENT_DATA_KEY_SUFFIX "_movementData"
#define SLEEP_DATA_KEY_SUFFIX "_sleepData"
#define STEP_TYPICALS_KEY_SUFFIX "_steps" // Not the best suffix, but we are stuck with it now...
#define STEP_AVERAGE_KEY_SUFFIX "_dailySteps"
#define SLEEP_AVERAGE_KEY_SUFFIX "_sleepDuration"
#define HR_ZONE_DATA_KEY_SUFFIX "_heartRateZoneData"
static const char *WEEKDAY_NAMES[] = {
[Sunday] = "sunday",
[Monday] = "monday",
[Tuesday] = "tuesday",
[Wednesday] = "wednesday",
[Thursday] = "thursday",
[Friday] = "friday",
[Saturday] = "saturday",
};
#define CURRENT_MOVEMENT_DATA_VERSION 1
#define CURRENT_SLEEP_DATA_VERSION 1
#define CURRENT_HR_ZONE_DATA_VERSION 1
typedef struct PACKED MovementData {
uint32_t version;
uint32_t last_processed_timestamp;
uint32_t steps;
uint32_t active_kcalories;
uint32_t resting_kcalories;
uint32_t distance;
uint32_t active_seconds;
} MovementData;
_Static_assert(offsetof(MovementData, version) == 0, "Version not at the start of MovementData");
_Static_assert(sizeof(MovementData) % sizeof(uint32_t) == 0, "MovementData size is invalid");
typedef struct PACKED SleepData {
uint32_t version;
uint32_t last_processed_timestamp;
uint32_t sleep_duration;
uint32_t deep_sleep_duration;
uint32_t fall_asleep_time;
uint32_t wakeup_time;
uint32_t typical_sleep_duration;
uint32_t typical_deep_sleep_duration;
uint32_t typical_fall_asleep_time;
uint32_t typical_wakeup_time;
} SleepData;
_Static_assert(offsetof(SleepData, version) == 0, "Version not at the start of SleepData");
_Static_assert(sizeof(SleepData) % sizeof(uint32_t) == 0, "SleepData size is invalid");
// The phone doesn't send us Zone0 minutes
typedef struct PACKED HeartRateZoneData {
uint32_t version;
uint32_t last_processed_timestamp;
uint32_t num_zones;
uint32_t minutes_in_zone[HRZone_Max];
} HeartRateZoneData;
_Static_assert(offsetof(HeartRateZoneData, version) == 0,
"Version not at the start of HeartRateZoneData");
_Static_assert(sizeof(HeartRateZoneData) % sizeof(uint32_t) == 0,
"HeartRateZoneData size is invalid");
static status_t prv_file_open_and_lock(SettingsFile *file) {
mutex_lock(s_mutex);
status_t rv = settings_file_open(file, HEALTH_DB_FILE_NAME, HEALTH_DB_MAX_SIZE);
if (rv != S_SUCCESS) {
PBL_LOG(LOG_LEVEL_ERROR, "Failed to open settings file");
mutex_unlock(s_mutex);
}
return rv;
}
static void prv_file_close_and_unlock(SettingsFile *file) {
settings_file_close(file);
mutex_unlock(s_mutex);
}
static bool prv_key_is_valid(const uint8_t *key, int key_len) {
return key_len != 0 && // invalid length
strchr((const char *)key, '_') != NULL; // invalid key
}
static bool prv_value_is_valid(const uint8_t *key,
int key_len,
const uint8_t *val,
int val_len) {
return val_len && val_len % sizeof(uint32_t) == 0;
}
static bool prv_is_last_processed_timestamp_valid(time_t timestamp) {
// We only store today + the last 6 days. Anything older than that should be ignored
const time_t start_of_today = time_start_of_today();
// This might not handle DST perfectly, but it should be good enough
const time_t oldest_valid_timestamp = start_of_today - (SECONDS_PER_DAY * 6);
if (timestamp < oldest_valid_timestamp || timestamp > start_of_today + SECONDS_PER_DAY) {
return false;
}
return true;
}
//! Tell the activity service that it needs to update its "current" values (non typicals / averages)
static void prv_notify_health_listeners(const char *key,
int key_len,
const uint8_t *val,
int val_len) {
DayInWeek wday;
for (wday = 0; wday < DAYS_PER_WEEK; wday++) {
if (strstr(key, WEEKDAY_NAMES[wday])) {
break;
}
}
// For logging
const DayInWeek cur_wday = time_util_get_day_in_week(rtc_get_time());
if (strstr(key, MOVEMENT_DATA_KEY_SUFFIX)) {
MovementData *data = (MovementData *)val;
if (!prv_is_last_processed_timestamp_valid(data->last_processed_timestamp)) {
return;
}
PBL_LOG(LOG_LEVEL_INFO, "Got MovementData for wday: %d, cur_wday: %d, steps: %"PRIu32"",
wday, cur_wday, data->steps);
activity_metrics_prv_set_metric(ActivityMetricStepCount, wday, data->steps);
activity_metrics_prv_set_metric(ActivityMetricActiveSeconds, wday, data->active_seconds);
activity_metrics_prv_set_metric(ActivityMetricRestingKCalories, wday, data->resting_kcalories);
activity_metrics_prv_set_metric(ActivityMetricActiveKCalories, wday, data->active_kcalories);
activity_metrics_prv_set_metric(ActivityMetricDistanceMeters, wday, data->distance);
} else if (strstr(key, SLEEP_DATA_KEY_SUFFIX)) {
SleepData *data = (SleepData *)val;
if (!prv_is_last_processed_timestamp_valid(data->last_processed_timestamp)) {
return;
}
PBL_LOG(LOG_LEVEL_INFO, "Got SleepData for wday: %d, cur_wday: %d, sleep: %"PRIu32"",
wday, cur_wday, data->sleep_duration);
activity_metrics_prv_set_metric(ActivityMetricSleepTotalSeconds, wday, data->sleep_duration);
activity_metrics_prv_set_metric(ActivityMetricSleepRestfulSeconds, wday,
data->deep_sleep_duration);
activity_metrics_prv_set_metric(ActivityMetricSleepEnterAtSeconds, wday,
data->fall_asleep_time);
activity_metrics_prv_set_metric(ActivityMetricSleepExitAtSeconds, wday, data->wakeup_time);
} else if (strstr(key, HR_ZONE_DATA_KEY_SUFFIX)) {
HeartRateZoneData *data = (HeartRateZoneData *)val;
if (!prv_is_last_processed_timestamp_valid(data->last_processed_timestamp)) {
return;
}
if (data->num_zones != HRZone_Max) {
return;
}
PBL_LOG(LOG_LEVEL_INFO, "Got HeartRateZoneData for wday: %d, cur_wday: %d, zone1: %"PRIu32"",
wday, cur_wday, data->minutes_in_zone[0]);
activity_metrics_prv_set_metric(ActivityMetricHeartRateZone1Minutes, wday,
data->minutes_in_zone[0]);
activity_metrics_prv_set_metric(ActivityMetricHeartRateZone2Minutes, wday,
data->minutes_in_zone[1]);
activity_metrics_prv_set_metric(ActivityMetricHeartRateZone3Minutes, wday,
data->minutes_in_zone[2]);
}
}
/////////////////////////
// Public API
/////////////////////////
bool health_db_get_typical_value(ActivityMetric metric,
DayInWeek day,
int32_t *value_out) {
char key[HEALTH_DB_MAX_KEY_LEN];
snprintf(key, HEALTH_DB_MAX_KEY_LEN, "%s%s", WEEKDAY_NAMES[day], SLEEP_DATA_KEY_SUFFIX);
const int key_len = strlen(key);
SettingsFile file;
if (prv_file_open_and_lock(&file) != S_SUCCESS) {
return false;
}
// We cheat a bit here because the only typical values we store are sleep related
SleepData data;
status_t s = settings_file_get(&file, key, key_len, (uint8_t *)&data, sizeof(data));
prv_file_close_and_unlock(&file);
if (s != S_SUCCESS || data.version != CURRENT_SLEEP_DATA_VERSION) {
return false;
}
switch (metric) {
case ActivityMetricSleepTotalSeconds:
*value_out = data.typical_sleep_duration;
break;
case ActivityMetricSleepRestfulSeconds:
*value_out = data.typical_deep_sleep_duration;
break;
case ActivityMetricSleepEnterAtSeconds:
*value_out = data.typical_fall_asleep_time;
break;
case ActivityMetricSleepExitAtSeconds:
*value_out = data.typical_wakeup_time;
break;
case ActivityMetricStepCount:
case ActivityMetricActiveSeconds:
case ActivityMetricRestingKCalories:
case ActivityMetricActiveKCalories:
case ActivityMetricDistanceMeters:
case ActivityMetricSleepStateSeconds:
case ActivityMetricLastVMC:
case ActivityMetricHeartRateRawBPM:
case ActivityMetricHeartRateRawQuality:
case ActivityMetricHeartRateRawUpdatedTimeUTC:
case ActivityMetricHeartRateFilteredBPM:
case ActivityMetricHeartRateFilteredUpdatedTimeUTC:
case ActivityMetricNumMetrics:
case ActivityMetricSleepState:
case ActivityMetricHeartRateZone1Minutes:
case ActivityMetricHeartRateZone2Minutes:
case ActivityMetricHeartRateZone3Minutes:
PBL_LOG(LOG_LEVEL_WARNING, "Health DB doesn't know about typical metric %d", metric);
return false;
}
return true;
}
bool health_db_get_monthly_average_value(ActivityMetric metric,
int32_t *value_out) {
if (metric != ActivityMetricStepCount && metric != ActivityMetricSleepTotalSeconds) {
PBL_LOG(LOG_LEVEL_WARNING, "Health DB doesn't store an average for metric %d", metric);
return false;
}
SettingsFile file;
if (prv_file_open_and_lock(&file) != S_SUCCESS) {
return false;
}
char key[HEALTH_DB_MAX_KEY_LEN];
snprintf(key, HEALTH_DB_MAX_KEY_LEN, "average%s", (metric == ActivityMetricStepCount) ?
STEP_AVERAGE_KEY_SUFFIX : SLEEP_AVERAGE_KEY_SUFFIX);
const int key_len = strlen(key);
status_t s = settings_file_get(&file, key, key_len, value_out, sizeof(uint32_t));
prv_file_close_and_unlock(&file);
return (s == S_SUCCESS);
}
bool health_db_get_typical_step_averages(DayInWeek day, ActivityMetricAverages *averages) {
if (!averages) {
return false;
}
// Default results
_Static_assert(((ACTIVITY_METRIC_AVERAGES_UNKNOWN >> 8) & 0xFF)
== (ACTIVITY_METRIC_AVERAGES_UNKNOWN & 0xFF), "Cannot use memset");
memset(averages->average, ACTIVITY_METRIC_AVERAGES_UNKNOWN & 0xFF, sizeof(averages->average));
SettingsFile file;
if (prv_file_open_and_lock(&file) != S_SUCCESS) {
return false;
}
char key[HEALTH_DB_MAX_KEY_LEN];
snprintf(key, HEALTH_DB_MAX_KEY_LEN, "%s%s", WEEKDAY_NAMES[day], STEP_TYPICALS_KEY_SUFFIX);
const int key_len = strlen(key);
status_t s = settings_file_get(&file, key, key_len, averages->average, sizeof(averages->average));
prv_file_close_and_unlock(&file);
return (s == S_SUCCESS);
}
//! For test / debug purposes only
bool health_db_set_typical_values(ActivityMetric metric,
DayInWeek day,
uint16_t *values,
int num_values) {
char key[HEALTH_DB_MAX_KEY_LEN];
snprintf(key, HEALTH_DB_MAX_KEY_LEN, "%s%s", WEEKDAY_NAMES[day], STEP_TYPICALS_KEY_SUFFIX);
const int key_len = strlen(key);
return health_db_insert((uint8_t *)key, key_len, (uint8_t*)values, num_values * sizeof(uint16_t));
}
/////////////////////////
// Blob DB API
/////////////////////////
void health_db_init(void) {
s_mutex = mutex_create();
PBL_ASSERTN(s_mutex != NULL);
}
status_t health_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
if (!prv_key_is_valid(key, key_len)) {
PBL_LOG(LOG_LEVEL_ERROR, "Invalid health db key");
PBL_HEXDUMP(LOG_LEVEL_ERROR, key, key_len);
return E_INVALID_ARGUMENT;
} else if (!prv_value_is_valid(key, key_len, val, val_len)) {
PBL_LOG(LOG_LEVEL_ERROR, "Invalid health db value. Length %d", val_len);
return E_INVALID_ARGUMENT;
}
#if HEALTH_DB_DEBUG
PBL_LOG(LOG_LEVEL_DEBUG, "New health db entry key:");
PBL_HEXDUMP(LOG_LEVEL_DEBUG, key, key_len);
PBL_LOG(LOG_LEVEL_DEBUG, "val: ");
PBL_HEXDUMP(LOG_LEVEL_DEBUG, val, val_len);
#endif
// Only store typicals / averages in this settings file. "Current" values are stored in the
// activity settings file.
// Sleep data contains a mix of current and typical values. The current values are just stored
// for convience and can't be accessed from this settings file.
status_t rv = S_SUCCESS;
if (!strstr((char *)key, MOVEMENT_DATA_KEY_SUFFIX)) {
SettingsFile file;
rv = prv_file_open_and_lock(&file);
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_set(&file, key, key_len, val, val_len);
prv_file_close_and_unlock(&file);
}
prv_notify_health_listeners((const char *)key, key_len, val, val_len);
return rv;
}
int health_db_get_len(const uint8_t *key, int key_len) {
if (!prv_key_is_valid(key, key_len)) {
return E_INVALID_ARGUMENT;
}
SettingsFile file;
status_t rv = prv_file_open_and_lock(&file);
if (rv != S_SUCCESS) {
return 0;
}
int length = settings_file_get_len(&file, key, key_len);
prv_file_close_and_unlock(&file);
return length;
}
status_t health_db_read(const uint8_t *key, int key_len, uint8_t *value_out, int value_out_len) {
if (!prv_key_is_valid(key, key_len)) {
return E_INVALID_ARGUMENT;
}
SettingsFile file;
status_t rv = prv_file_open_and_lock(&file);
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_get(&file, key, key_len, value_out, value_out_len);
prv_file_close_and_unlock(&file);
return rv;
}
status_t health_db_delete(const uint8_t *key, int key_len) {
if (!prv_key_is_valid(key, key_len)) {
return E_INVALID_ARGUMENT;
}
SettingsFile file;
status_t rv = prv_file_open_and_lock(&file);
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_delete(&file, key, key_len);
prv_file_close_and_unlock(&file);
return rv;
}
status_t health_db_flush(void) {
mutex_lock(s_mutex);
status_t rv = pfs_remove(HEALTH_DB_FILE_NAME);
mutex_unlock(s_mutex);
return rv;
}

View file

@ -0,0 +1,60 @@
/*
* 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/normal/activity/activity.h"
#include "system/status_codes.h"
#include "util/attributes.h"
//! Get the typical metric value for a given day.
//! If you want "typical steps" you probably want health_db_get_typical_step_averages
bool health_db_get_typical_value(ActivityMetric metric,
DayInWeek day,
int32_t *value_out);
//! Get the average metric value over the last month
bool health_db_get_monthly_average_value(ActivityMetric metric,
int32_t *value_out);
//! Often referred to as "typical steps"
bool health_db_get_typical_step_averages(DayInWeek day,
ActivityMetricAverages *averages);
//! For test / debug purposes only
bool health_db_set_typical_values(ActivityMetric metric,
DayInWeek day,
uint16_t *values,
int num_values);
///////////////////////////////////////////
// BlobDB Boilerplate (see blob_db/api.h)
///////////////////////////////////////////
void health_db_init(void);
status_t health_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
int health_db_get_len(const uint8_t *key, int key_len);
status_t health_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
status_t health_db_delete(const uint8_t *key, int key_len);
status_t health_db_flush(void);

View file

@ -0,0 +1,409 @@
/*
* 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 "ios_notif_pref_db.h"
#include "sync.h"
#include "sync_util.h"
#include "console/prompt.h"
#include "kernel/pbl_malloc.h"
#include "os/mutex.h"
#include "services/normal/filesystem/pfs.h"
#include "services/normal/settings/settings_file.h"
#include "services/normal/timeline/attributes_actions.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/attributes.h"
#include "util/units.h"
#include <stdio.h>
T_STATIC const char *iOS_NOTIF_PREF_DB_FILE_NAME = "iosnotifprefdb";
T_STATIC const int iOS_NOTIF_PREF_MAX_SIZE = KiBYTES(10);
typedef struct PACKED {
uint32_t flags;
uint8_t num_attributes;
uint8_t num_actions;
uint8_t data[]; // Serialized attributes followed by serialized actions
} SerializedNotifPrefs;
static PebbleMutex *s_mutex;
static status_t prv_file_open_and_lock(SettingsFile *file) {
mutex_lock(s_mutex);
status_t rv = settings_file_open(file, iOS_NOTIF_PREF_DB_FILE_NAME, iOS_NOTIF_PREF_MAX_SIZE);
if (rv != S_SUCCESS) {
mutex_unlock(s_mutex);
}
return rv;
}
static void prv_file_close_and_unlock(SettingsFile *file) {
settings_file_close(file);
mutex_unlock(s_mutex);
}
//! Assumes the file is opened and locked
static status_t prv_save_serialized_prefs(SettingsFile *file, const void *key, size_t key_len,
const void *val, size_t val_len) {
// Invert flags before writing to flash
((SerializedNotifPrefs *)val)->flags = ~(((SerializedNotifPrefs *)val)->flags);
return settings_file_set(file, key, key_len, val, val_len);
}
//! Assumes the file is opened and locked
static status_t prv_read_serialized_prefs(SettingsFile *file, const void *key, size_t key_len,
void *val_out, size_t val_out_len) {
status_t rv = settings_file_get(file, key, key_len, val_out, val_out_len);
// The flags for inverted before writing, revert them back
((SerializedNotifPrefs *)val_out)->flags = ~(((SerializedNotifPrefs *)val_out)->flags);
return rv;
}
//! Returns the length of the data
//! When done with the prefs, call prv_free_serialzed_prefs()
static int prv_get_serialized_prefs(SettingsFile *file, const uint8_t *app_id, int key_len,
SerializedNotifPrefs **prefs_out) {
const unsigned prefs_len = settings_file_get_len(file, app_id, key_len);
if (prefs_len < sizeof(SerializedNotifPrefs)) {
return 0;
}
*prefs_out = kernel_zalloc(prefs_len);
if (!*prefs_out) {
return 0;
}
status_t rv = prv_read_serialized_prefs(file, app_id, key_len, (void *) *prefs_out, prefs_len);
if (rv != S_SUCCESS) {
kernel_free(*prefs_out);
return 0;
}
return (prefs_len - sizeof(SerializedNotifPrefs));
}
static void prv_free_serialzed_prefs(SerializedNotifPrefs *prefs) {
kernel_free(prefs);
}
iOSNotifPrefs* ios_notif_pref_db_get_prefs(const uint8_t *app_id, int key_len) {
SettingsFile file;
status_t rv = prv_file_open_and_lock(&file);
if (rv != S_SUCCESS) {
return 0;
}
if (!settings_file_exists(&file, app_id, key_len)) {
char buffer[key_len + 1];
strncpy(buffer, (const char *)app_id, key_len);
buffer[key_len] = '\0';
PBL_LOG(LOG_LEVEL_DEBUG, "No prefs found for <%s>", buffer);
prv_file_close_and_unlock(&file);
return NULL;
}
SerializedNotifPrefs *serialized_prefs = NULL;
const int serialized_prefs_data_len = prv_get_serialized_prefs(&file, app_id, key_len,
&serialized_prefs);
prv_file_close_and_unlock(&file);
size_t string_alloc_size;
uint8_t attributes_per_action[serialized_prefs->num_actions];
bool r = attributes_actions_parse_serial_data(serialized_prefs->num_attributes,
serialized_prefs->num_actions,
serialized_prefs->data,
serialized_prefs_data_len,
&string_alloc_size,
attributes_per_action);
if (!r) {
char buffer[key_len + 1];
strncpy(buffer, (const char *)app_id, key_len);
buffer[key_len] = '\0';
PBL_LOG(LOG_LEVEL_ERROR, "Could not parse serial data for <%s>", buffer);
prv_free_serialzed_prefs(serialized_prefs);
return NULL;
}
const size_t alloc_size =
attributes_actions_get_required_buffer_size(serialized_prefs->num_attributes,
serialized_prefs->num_actions,
attributes_per_action,
string_alloc_size);
iOSNotifPrefs *notif_prefs = kernel_zalloc_check(sizeof(iOSNotifPrefs) + alloc_size);
uint8_t *buffer = (uint8_t *)notif_prefs + sizeof(iOSNotifPrefs);
uint8_t *const buf_end = buffer + alloc_size;
attributes_actions_init(&notif_prefs->attr_list, &notif_prefs->action_group,
&buffer, serialized_prefs->num_attributes, serialized_prefs->num_actions,
attributes_per_action);
if (!attributes_actions_deserialize(&notif_prefs->attr_list,
&notif_prefs->action_group,
buffer,
buf_end,
serialized_prefs->data,
serialized_prefs_data_len)) {
char buffer[key_len + 1];
strncpy(buffer, (const char *)app_id, key_len);
buffer[key_len] = '\0';
PBL_LOG(LOG_LEVEL_ERROR, "Could not deserialize data for <%s>", buffer);
prv_free_serialzed_prefs(serialized_prefs);
kernel_free(notif_prefs);
return NULL;
}
prv_free_serialzed_prefs(serialized_prefs);
return notif_prefs;
}
void ios_notif_pref_db_free_prefs(iOSNotifPrefs *prefs) {
kernel_free(prefs);
}
status_t ios_notif_pref_db_store_prefs(const uint8_t *app_id, int length, AttributeList *attr_list,
TimelineItemActionGroup *action_group) {
SettingsFile file;
status_t rv = prv_file_open_and_lock(&file);
if (rv != S_SUCCESS) {
return rv;
}
size_t payload_size = attributes_actions_get_serialized_payload_size(attr_list, action_group);
size_t serialized_prefs_size = sizeof(SerializedNotifPrefs) + payload_size;
SerializedNotifPrefs *new_prefs = kernel_zalloc_check(serialized_prefs_size);
*new_prefs = (SerializedNotifPrefs) {
.num_attributes = attr_list ? attr_list->num_attributes : 0,
.num_actions = action_group ? action_group->num_actions : 0,
};
attributes_actions_serialize_payload(attr_list, action_group, new_prefs->data, payload_size);
// Add the new entry to the DB
rv = prv_save_serialized_prefs(&file, app_id, length, new_prefs, serialized_prefs_size);
prv_file_close_and_unlock(&file);
if (rv == S_SUCCESS) {
char buffer[length + 1];
strncpy(buffer, (const char *)app_id, length);
buffer[length] = '\0';
PBL_LOG(LOG_LEVEL_INFO, "Added <%s> to the notif pref db", buffer);
blob_db_sync_record(BlobDBIdiOSNotifPref, app_id, length, rtc_get_time());
}
return rv;
}
void ios_notif_pref_db_init(void) {
s_mutex = mutex_create();
PBL_ASSERTN(s_mutex != NULL);
}
status_t ios_notif_pref_db_insert(const uint8_t *key, int key_len,
const uint8_t *val, int val_len) {
if (key_len == 0 || val_len == 0 || val_len < (int) sizeof(SerializedNotifPrefs)) {
return E_INVALID_ARGUMENT;
}
SettingsFile file;
status_t rv = prv_file_open_and_lock(&file);
if (rv != S_SUCCESS) {
return rv;
}
rv = prv_save_serialized_prefs(&file, key, key_len, val, val_len);
if (rv == S_SUCCESS) {
char buffer[key_len + 1];
strncpy(buffer, (const char *)key, key_len);
buffer[key_len] = '\0';
PBL_LOG(LOG_LEVEL_INFO, "iOS notif pref insert <%s>", buffer);
// All records inserted from the phone are not dirty (the phone is the source of truth)
rv = settings_file_mark_synced(&file, key, key_len);
}
prv_file_close_and_unlock(&file);
return rv;
}
int ios_notif_pref_db_get_len(const uint8_t *key, int key_len) {
if (key_len == 0) {
return 0;
}
SettingsFile file;
status_t rv = prv_file_open_and_lock(&file);
if (rv != S_SUCCESS) {
return 0;
}
int length = settings_file_get_len(&file, key, key_len);
prv_file_close_and_unlock(&file);
return length;
}
status_t ios_notif_pref_db_read(const uint8_t *key, int key_len,
uint8_t *val_out, int val_out_len) {
SettingsFile file;
status_t rv = prv_file_open_and_lock(&file);
if (rv != S_SUCCESS) {
return rv;
}
rv = prv_read_serialized_prefs(&file, key, key_len, val_out, val_out_len);
prv_file_close_and_unlock(&file);
return rv;
}
status_t ios_notif_pref_db_delete(const uint8_t *key, int key_len) {
if (key_len == 0) {
return E_INVALID_ARGUMENT;
}
SettingsFile file;
status_t rv = prv_file_open_and_lock(&file);
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_delete(&file, key, key_len);
prv_file_close_and_unlock(&file);
return rv;
}
status_t ios_notif_pref_db_flush(void) {
mutex_lock(s_mutex);
status_t rv = pfs_remove(iOS_NOTIF_PREF_DB_FILE_NAME);
mutex_unlock(s_mutex);
return rv;
}
status_t ios_notif_pref_db_is_dirty(bool *is_dirty_out) {
SettingsFile file;
status_t rv = prv_file_open_and_lock(&file);
if (rv != S_SUCCESS) {
return rv;
}
*is_dirty_out = false;
rv = settings_file_each(&file, sync_util_is_dirty_cb, is_dirty_out);
prv_file_close_and_unlock(&file);
return rv;
}
BlobDBDirtyItem* ios_notif_pref_db_get_dirty_list(void) {
SettingsFile file;
if (S_SUCCESS != prv_file_open_and_lock(&file)) {
return NULL;
}
BlobDBDirtyItem *dirty_list = NULL;
settings_file_each(&file, sync_util_build_dirty_list_cb, &dirty_list);
prv_file_close_and_unlock(&file);
return dirty_list;
}
status_t ios_notif_pref_db_mark_synced(const uint8_t *key, int key_len) {
if (key_len == 0) {
return E_INVALID_ARGUMENT;
}
SettingsFile file;
status_t rv = prv_file_open_and_lock(&file);
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_mark_synced(&file, key, key_len);
prv_file_close_and_unlock(&file);
return rv;
}
// ----------------------------------------------------------------------------------------------
#if UNITTEST
uint32_t ios_notif_pref_db_get_flags(const uint8_t *app_id, int key_len) {
SettingsFile file;
status_t rv = prv_file_open_and_lock(&file);
if (rv != S_SUCCESS) {
return 0;
}
SerializedNotifPrefs *prefs = NULL;
prv_get_serialized_prefs(&file, app_id, key_len, &prefs);
uint32_t flags = prefs->flags;
prv_free_serialzed_prefs(prefs);
prv_file_close_and_unlock(&file);
return flags;
}
#endif
// ----------------------------------------------------------------------------------------------
static bool prv_print_notif_pref_db(SettingsFile *file, SettingsRecordInfo *info, void *context) {
char app_id[64];
info->get_key(file, app_id, info->key_len);
app_id[info->key_len] = '\0';
prompt_send_response(app_id);
char buffer[64];
prompt_send_response_fmt(buffer, sizeof(buffer), "Dirty: %s", info->dirty ? "Yes" : "No");
prompt_send_response_fmt(buffer, sizeof(buffer), "Last modified: %"PRIu32"", info->last_modified);
SerializedNotifPrefs *serialized_prefs = NULL;
prv_get_serialized_prefs(file, (uint8_t *)app_id, info->key_len, &serialized_prefs);
prompt_send_response_fmt(buffer, sizeof(buffer), "Attributes: %d, Actions: %d",
serialized_prefs->num_attributes, serialized_prefs->num_actions);
// TODO: Print the attributes and actions
prv_free_serialzed_prefs(serialized_prefs);
prompt_send_response("");
return true;
}
void command_dump_notif_pref_db(void) {
SettingsFile file;
if (S_SUCCESS != prv_file_open_and_lock(&file)) {
return;
}
settings_file_each(&file, prv_print_notif_pref_db, NULL);
prv_file_close_and_unlock(&file);
}

View file

@ -0,0 +1,81 @@
/*
* 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 "api.h"
#include "services/normal/timeline/attribute.h"
#include "services/normal/timeline/item.h"
#include "system/status_codes.h"
#include <stdbool.h>
//! The iOS Pebble app doesn't have much control over the notification experience.
//! The watch receives notifications directly from ANCS, so the iOS app doesn't get a
//! chance to do any processing or filtering.
//! This db stores preferences on different types of notifications so the FW can perform
//! some processing / filtering.
typedef struct {
AttributeList attr_list;
TimelineItemActionGroup action_group;
} iOSNotifPrefs;
//! @param app_id The iOS app id to check. ex com.apple.MobileSMS
//! @param length The length of the app_id
//! @return A pointer to the prefs, NULL if none are available
//! @note The caller must cleanup with ios_notif_pref_db_free_prefs()
iOSNotifPrefs* ios_notif_pref_db_get_prefs(const uint8_t *app_id, int length);
//! @param prefs A pointer to prefs returned by ios_notif_pref_db_get_prefs()
void ios_notif_pref_db_free_prefs(iOSNotifPrefs *prefs);
//! Adds or updates a record in the notif_pref_db.
//! @param app_id The iOS app id to check. ex com.apple.MobileSMS
//! @param length The length of the app_id
//! @param attr_list AttributeList for the app
//! @param attr_list ActionGroup for the app
status_t ios_notif_pref_db_store_prefs(const uint8_t *app_id, int length, AttributeList *attr_list,
TimelineItemActionGroup *action_group);
///////////////////////////////////////////
// BlobDB Boilerplate (see blob_db/api.h)
///////////////////////////////////////////
void ios_notif_pref_db_init(void);
status_t ios_notif_pref_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
int ios_notif_pref_db_get_len(const uint8_t *key, int key_len);
status_t ios_notif_pref_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
status_t ios_notif_pref_db_delete(const uint8_t *key, int key_len);
status_t ios_notif_pref_db_flush(void);
status_t ios_notif_pref_db_is_dirty(bool *is_dirty_out);
BlobDBDirtyItem* ios_notif_pref_db_get_dirty_list(void);
status_t ios_notif_pref_db_mark_synced(const uint8_t *key, int key_len);
#if UNITTEST
uint32_t ios_notif_pref_db_get_flags(const uint8_t *app_id, int key_len);
#endif

View file

@ -0,0 +1,93 @@
/*
* 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 "notif_db.h"
#include "kernel/pbl_malloc.h"
#include "services/normal/notifications/notification_storage.h"
#include "system/logging.h"
void notif_db_init(void) {
}
status_t notif_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
if (key_len != UUID_SIZE ||
val_len < (int)sizeof(SerializedTimelineItemHeader)) {
return E_INVALID_ARGUMENT;
}
// [FBO] this is a little bit silly: we deserialize the item to then re-serialize it in
// notification_storage_store. It has the advantage that it validates the payload
// and works with the existing storage
SerializedTimelineItemHeader *hdr = (SerializedTimelineItemHeader *)val;
const uint8_t *payload = val + sizeof(SerializedTimelineItemHeader);
const bool has_status_bits = (hdr->common.status != 0);
TimelineItem notification = {};
if (!timeline_item_deserialize_item(&notification, hdr, payload)) {
return E_INTERNAL;
}
Uuid *id = kernel_malloc_check(sizeof(Uuid));
*id = notification.header.id;
char uuid_string[UUID_STRING_BUFFER_LENGTH];
uuid_to_string(id, uuid_string);
// If the notification already exists, only update the status flags
if (notification_storage_notification_exists(&notification.header.id)) {
notification_storage_set_status(&notification.header.id, notification.header.status);
PBL_LOG(LOG_LEVEL_INFO, "Notification modified: %s", uuid_string);
notifications_handle_notification_acted_upon(id);
} else if (!has_status_bits) {
notification_storage_store(&notification);
PBL_LOG(LOG_LEVEL_INFO, "Notification added: %s", uuid_string);
notifications_handle_notification_added(id);
}
timeline_item_free_allocated_buffer(&notification);
return S_SUCCESS;
}
int notif_db_get_len(const uint8_t *key, int key_len) {
if (key_len < UUID_SIZE) {
return 0;
}
return notification_storage_get_len((Uuid *)key);
}
status_t notif_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len) {
// NYI
return S_SUCCESS;
}
status_t notif_db_delete(const uint8_t *key, int key_len) {
if (key_len != UUID_SIZE) {
return E_INVALID_ARGUMENT;
}
notification_storage_remove((Uuid *)key);
notifications_handle_notification_removed((Uuid *)key);
return S_SUCCESS;
}
status_t notif_db_flush(void) {
notification_storage_reset_and_init();
return S_SUCCESS;
}

View file

@ -0,0 +1,36 @@
/*
* 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 "system/status_codes.h"
#include "services/normal/timeline/item.h"
///////////////////////////////////////////
// BlobDB Boilerplate (see blob_db/api.h)
///////////////////////////////////////////
void notif_db_init(void);
status_t notif_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
int notif_db_get_len(const uint8_t *key, int key_len);
status_t notif_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
status_t notif_db_delete(const uint8_t *key, int key_len);
status_t notif_db_flush(void);

View file

@ -0,0 +1,276 @@
/*
* 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 "api.h"
#include "pin_db.h"
#include "reminder_db.h"
#include "sync.h"
#include "sync_util.h"
#include "timeline_item_storage.h"
#include <string.h>
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "process_management/app_install_manager.h"
#include "services/normal/app_cache.h"
#include "services/normal/timeline/calendar.h"
#include "services/normal/timeline/timeline.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/units.h"
#include "util/uuid.h"
#define PIN_DB_MAX_AGE (3 * SECONDS_PER_DAY) // so we get at two full past days in there
#define PIN_DB_FILE_NAME "pindb"
#define PIN_DB_MAX_SIZE KiBYTES(40) // TODO [FBO] variable size / reasonable value
static TimelineItemStorage s_pin_db_storage;
/////////////////////////
// Pin DB specific API
/////////////////////////
status_t pin_db_delete_with_parent(const TimelineItemId *parent_id) {
return (timeline_item_storage_delete_with_parent(&s_pin_db_storage, parent_id, NULL));
}
//! Caution: CommonTimelineItemHeader .flags & .status are stored inverted and not auto-restored
status_t pin_db_each(SettingsFileEachCallback each, void *data) {
return timeline_item_storage_each(&s_pin_db_storage, each, data);
}
static status_t prv_insert_serialized_item(const uint8_t *key, int key_len, const uint8_t *val,
int val_len, bool mark_synced) {
CommonTimelineItemHeader *hdr = (CommonTimelineItemHeader *)val;
if (hdr->layout == LayoutIdNotification || hdr->layout == LayoutIdReminder) {
// pins do not support these layouts
return E_INVALID_ARGUMENT;
}
status_t rv = timeline_item_storage_insert(&s_pin_db_storage, key, key_len,
val, val_len, mark_synced);
if (rv == S_SUCCESS) {
TimelineItemId parent_id = ((CommonTimelineItemHeader *)val)->parent_id;
if (timeline_get_private_data_source(&parent_id)) {
goto done;
}
// Not a private data source, must be a PBW
AppInstallId install_id = app_install_get_id_for_uuid(&parent_id);
// can't add a pin for a not installed app!
if (install_id == INSTALL_ID_INVALID) {
// String initialized on the heap to reduce stack usage
char *parent_id_string = kernel_malloc_check(UUID_STRING_BUFFER_LENGTH);
uuid_to_string(&parent_id, parent_id_string);
PBL_LOG(LOG_LEVEL_ERROR,
"Pin insert for a pin with no app installed, parent id: %s",
parent_id_string);
kernel_free(parent_id_string);
goto done;
}
// Bump the app's priority by telling the cache we're using it
if (app_cache_entry_exists(install_id)) {
app_cache_app_launched(install_id);
goto done;
}
// System apps don't need to be fetched / are always installed
if (app_install_id_from_system(install_id)) {
goto done;
}
// The app isn't cached. Fetch it!
PebbleEvent e = {
.type = PEBBLE_APP_FETCH_REQUEST_EVENT,
.app_fetch_request = {
.id = install_id,
.with_ui = false,
.fetch_args = NULL,
},
};
event_put(&e);
}
done:
return rv;
}
static status_t prv_insert_item(TimelineItem *item, bool emit_event) {
if (item->header.type != TimelineItemTypePin) {
return E_INVALID_ARGUMENT;
}
// allocate a buffer big enough for serialized item
size_t payload_size = timeline_item_get_serialized_payload_size(item);
uint8_t *buffer = kernel_malloc_check(sizeof(SerializedTimelineItemHeader) + payload_size);
uint8_t *write_ptr = buffer;
// serialize the header
timeline_item_serialize_header(item, (SerializedTimelineItemHeader *) write_ptr);
write_ptr += sizeof(SerializedTimelineItemHeader);
// serialize the attributes / actions
size_t bytes_serialized = timeline_item_serialize_payload(item, write_ptr, payload_size);
status_t rv;
if (bytes_serialized != payload_size) {
rv = E_INVALID_ARGUMENT;
goto cleanup;
}
// Only pins from the reminders app should be dirty and synced to the phone
Uuid reminders_data_source_uuid = UUID_REMINDERS_DATA_SOURCE;
const bool mark_synced = !uuid_equal(&item->header.parent_id, &reminders_data_source_uuid);
rv = prv_insert_serialized_item(
(uint8_t *)&item->header.id, sizeof(TimelineItemId),
buffer, sizeof(SerializedTimelineItemHeader) + payload_size, mark_synced);
if (rv == S_SUCCESS && emit_event) {
blob_db_event_put(BlobDBEventTypeInsert, BlobDBIdPins, (uint8_t *)&item->header.id,
sizeof(TimelineItemId));
}
if (!mark_synced) {
blob_db_sync_record(BlobDBIdPins, (uint8_t *)&item->header.id, sizeof(TimelineItemId),
rtc_get_time());
}
cleanup:
kernel_free(buffer);
return rv;
}
status_t pin_db_insert_item(TimelineItem *item) {
return prv_insert_item(item, true /* emit_event */);
}
status_t pin_db_insert_item_without_event(TimelineItem *item) {
return prv_insert_item(item, false /* emit_event */);
}
status_t pin_db_set_status_bits(const TimelineItemId *id, uint8_t status) {
return timeline_item_storage_set_status_bits(&s_pin_db_storage, (uint8_t *)id, sizeof(*id),
status);
}
status_t pin_db_get(const TimelineItemId *id, TimelineItem *pin) {
int size = pin_db_get_len((uint8_t *)id, UUID_SIZE);
if (size <= 0) {
return E_DOES_NOT_EXIST;
}
uint8_t *read_buf = task_malloc_check(size);
status_t status = pin_db_read((uint8_t *)id, UUID_SIZE, read_buf, size);
if (status != S_SUCCESS) {
goto cleanup;
}
SerializedTimelineItemHeader *header = (SerializedTimelineItemHeader *)read_buf;
uint8_t *payload = read_buf + sizeof(SerializedTimelineItemHeader);
if (!timeline_item_deserialize_item(pin, header, payload)) {
status = E_INTERNAL;
goto cleanup;
}
task_free(read_buf);
return (S_SUCCESS);
cleanup:
task_free(read_buf);
return status;
}
bool pin_db_exists_with_parent(const TimelineItemId *parent_id) {
return timeline_item_storage_exists_with_parent(&s_pin_db_storage, parent_id);
}
status_t pin_db_read_item_header(TimelineItem *item_out, TimelineItemId *id) {
SerializedTimelineItemHeader hdr = {{{0}}};
status_t rv = pin_db_read((uint8_t *)id, sizeof(TimelineItemId), (uint8_t *)&hdr,
sizeof(SerializedTimelineItemHeader));
timeline_item_deserialize_header(item_out, &hdr);
return rv;
}
status_t pin_db_next_item_header(TimelineItem *next_item_out,
TimelineItemStorageFilterCallback filter) {
TimelineItemId id;
status_t rv = timeline_item_storage_next_item(&s_pin_db_storage, &id, filter);
if (rv) {
return rv;
}
rv = pin_db_read_item_header(next_item_out, &id);
return rv;
}
/////////////////////////
// Blob DB API
/////////////////////////
void pin_db_init(void) {
s_pin_db_storage = (TimelineItemStorage){};
timeline_item_storage_init(&s_pin_db_storage,
PIN_DB_FILE_NAME,
PIN_DB_MAX_SIZE,
PIN_DB_MAX_AGE);
}
void pin_db_deinit(void) {
timeline_item_storage_deinit(&s_pin_db_storage);
}
bool pin_db_has_entry_expired(time_t pin_end_timestamp) {
return (pin_end_timestamp < (rtc_get_time() - PIN_DB_MAX_AGE));
}
status_t pin_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
// Records inserted from the phone are already synced
const bool mark_synced = true;
return prv_insert_serialized_item(key, key_len, val, val_len, mark_synced);
}
int pin_db_get_len(const uint8_t *key, int key_len) {
return timeline_item_storage_get_len(&s_pin_db_storage, key, key_len);
}
status_t pin_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_len) {
return timeline_item_storage_read(&s_pin_db_storage, key, key_len, val_out, val_len);
}
status_t pin_db_delete(const uint8_t *key, int key_len) {
status_t rv = timeline_item_storage_delete(&s_pin_db_storage, key, key_len);
if (rv == S_SUCCESS) {
//! remove reminders that are children of this pin
reminder_db_delete_with_parent((TimelineItemId *)key);
}
return rv;
}
status_t pin_db_flush(void) {
return timeline_item_storage_flush(&s_pin_db_storage);
}
status_t pin_db_is_dirty(bool *is_dirty_out) {
*is_dirty_out = false;
return timeline_item_storage_each(&s_pin_db_storage, sync_util_is_dirty_cb, is_dirty_out);
}
BlobDBDirtyItem* pin_db_get_dirty_list(void) {
BlobDBDirtyItem *dirty_list = NULL;
timeline_item_storage_each(&s_pin_db_storage, sync_util_build_dirty_list_cb, &dirty_list);
return dirty_list;
}
status_t pin_db_mark_synced(const uint8_t *key, int key_len) {
return timeline_item_storage_mark_synced(&s_pin_db_storage, key, key_len);
}

View file

@ -0,0 +1,78 @@
/*
* 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 "api.h"
#include "timeline_item_storage.h"
#include "system/status_codes.h"
#include "services/normal/timeline/item.h"
#include "util/iterator.h"
#include <stdint.h>
status_t pin_db_get(const TimelineItemId *id, TimelineItem *pin);
status_t pin_db_insert_item(TimelineItem *item);
//! Inserts an item without emitting a BlobDB event.
//! @note This is provided for testing automatically generated pins which would otherwise flood
//! the event queue. Please use \ref pin_db_insert_item instead when possible.
status_t pin_db_insert_item_without_event(TimelineItem *item);
status_t pin_db_set_status_bits(const TimelineItemId *id, uint8_t status);
//! Caution: CommonTimelineItemHeader .flags & .status are stored inverted and not auto-restored
status_t pin_db_each(TimelineItemStorageEachCallback each, void *data);
status_t pin_db_delete_with_parent(const TimelineItemId *parent_id);
bool pin_db_exists_with_parent(const TimelineItemId *parent_id);
status_t pin_db_read_item_header(TimelineItem *item_out, TimelineItemId *id);
status_t pin_db_next_item_header(TimelineItem *next_item_out,
TimelineItemStorageFilterCallback filter);
//! Determines whether or not the timeline entry has expired based on its age
//! @param pin_timestamp - the timestamp of the pin being removed
bool pin_db_has_entry_expired(time_t pin_end_timestamp);
///////////////////////////////////////////
// BlobDB Boilerplate (see blob_db/api.h)
///////////////////////////////////////////
void pin_db_init(void);
void pin_db_deinit(void);
status_t pin_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
int pin_db_get_len(const uint8_t *key, int key_len);
status_t pin_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
status_t pin_db_delete(const uint8_t *key, int key_len);
status_t pin_db_flush(void);
status_t pin_db_is_dirty(bool *is_dirty_out);
BlobDBDirtyItem* pin_db_get_dirty_list(void);
status_t pin_db_mark_synced(const uint8_t *key, int key_len);

View file

@ -0,0 +1,60 @@
/*
* 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 "prefs_db.h"
#include "process_management/app_install_manager.h"
#include "shell/prefs_private.h"
// BlobDB APIs
////////////////////////////////////////////////////////////////////////////////
void prefs_db_init(void) {
}
status_t prefs_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
bool success = prefs_private_write_backing(key, key_len, val, val_len);
if (success) {
return S_SUCCESS;
} else {
return E_INVALID_ARGUMENT;
}
}
int prefs_db_get_len(const uint8_t *key, int key_len) {
size_t len = prefs_private_get_backing_len(key, key_len);
if (len == 0) {
return E_INVALID_ARGUMENT;
}
return len;
}
status_t prefs_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len) {
bool success = prefs_private_read_backing(key, key_len, val_out, val_out_len);
if (success) {
return S_SUCCESS;
} else {
return E_INVALID_ARGUMENT;
}
}
status_t prefs_db_delete(const uint8_t *key, int key_len) {
return E_INVALID_OPERATION;
}
status_t prefs_db_flush(void) {
return E_INVALID_OPERATION;
}

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.
*/
#pragma once
#include "system/status_codes.h"
///////////////////////////////////////////
// BlobDB Boilerplate (see blob_db/api.h)
///////////////////////////////////////////
void prefs_db_init(void);
status_t prefs_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
int prefs_db_get_len(const uint8_t *key, int key_len);
status_t prefs_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
status_t prefs_db_delete(const uint8_t *key, int key_len);
status_t prefs_db_flush(void);

View file

@ -0,0 +1,284 @@
/*
* 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 "reminder_db.h"
#include "sync.h"
#include "sync_util.h"
#include "timeline_item_storage.h"
#include "util/uuid.h"
#include "kernel/pbl_malloc.h"
#include "services/common/analytics/analytics.h"
#include "services/normal/timeline/reminders.h"
#include "system/passert.h"
#include "system/logging.h"
#include "util/units.h"
#define REMINDER_DB_FILE_NAME "reminderdb"
#define REMINDER_DB_MAX_SIZE KiBYTES(40)
#define MAX_REMINDER_SIZE SETTINGS_VAL_MAX_LEN
#define MAX_REMINDER_AGE (15 * SECONDS_PER_MINUTE)
typedef struct {
TimelineItemStorageFilterCallback filter_cb;
time_t timestamp;
const char *title;
TimelineItem *reminder_out;
bool match;
} ReminderInfo;
static TimelineItemStorage s_storage;
static status_t prv_read_item_header(TimelineItem *item_out, TimelineItemId *id) {
SerializedTimelineItemHeader hdr = {{{0}}};
status_t rv = reminder_db_read((uint8_t *)id, sizeof(TimelineItemId), (uint8_t *)&hdr,
sizeof(SerializedTimelineItemHeader));
timeline_item_deserialize_header(item_out, &hdr);
return rv;
}
/////////////////////////
// Reminder DB specific API
/////////////////////////
status_t reminder_db_delete_with_parent(const TimelineItemId *parent_id) {
return (timeline_item_storage_delete_with_parent(&s_storage, parent_id,
reminders_handle_reminder_removed));
}
status_t reminder_db_read_item(TimelineItem *item_out, TimelineItemId *id) {
size_t size = reminder_db_get_len((uint8_t *)id, sizeof(TimelineItemId));
if (size == 0) {
return E_DOES_NOT_EXIST;
}
uint8_t *read_buf = kernel_malloc_check(size);
status_t rv = reminder_db_read((uint8_t *)id, sizeof(TimelineItemId), read_buf, size);
if (rv != S_SUCCESS) {
goto cleanup;
}
SerializedTimelineItemHeader *header = (SerializedTimelineItemHeader *)read_buf;
uint8_t *payload = read_buf + sizeof(SerializedTimelineItemHeader);
if (!timeline_item_deserialize_item(item_out, header, payload)) {
rv = E_INTERNAL;
goto cleanup;
}
kernel_free(read_buf);
return S_SUCCESS;
cleanup:
kernel_free(read_buf);
return rv;
}
// Only keep reminders that have not been fired yet.
static bool prv_reminder_filter(SerializedTimelineItemHeader *hdr, void *context) {
return ((TimelineItemStatusReminded & hdr->common.status) == 0);
}
status_t reminder_db_next_item_header(TimelineItem *next_item_out) {
PBL_LOG(LOG_LEVEL_DEBUG, "Finding next item in queue.");
TimelineItemId id;
status_t rv = timeline_item_storage_next_item(&s_storage, &id, prv_reminder_filter);
if (rv) {
return rv;
}
rv = prv_read_item_header(next_item_out, &id);
return rv;
}
static bool prv_timestamp_title_compare_func(SettingsFile *file, SettingsRecordInfo *info,
void *context) {
// Check entry is valid
if (info->key_len != UUID_SIZE || info->val_len == 0) {
return true; // continue iteration
}
// Compare timestamps (this should omit most reminders)
ReminderInfo *reminder_info = (ReminderInfo *)context;
SerializedTimelineItemHeader header;
info->get_val(file, (uint8_t *)&header, sizeof(SerializedTimelineItemHeader));
if (reminder_info->timestamp != header.common.timestamp) {
return true; // continue iteration
}
// Read the full reminder to compare text
TimelineItem *reminder = reminder_info->reminder_out;
if (timeline_item_storage_get_from_settings_record(file, info, reminder) != S_SUCCESS) {
return true; // continue iteration
}
const char *title = attribute_get_string(&reminder->attr_list, AttributeIdTitle, "");
if (strcmp(title, reminder_info->title) != 0) {
timeline_item_free_allocated_buffer(reminder);
return true; // continue iteration
}
if (reminder_info->filter_cb && !reminder_info->filter_cb(&header, context)) {
timeline_item_free_allocated_buffer(reminder);
return true; // continue iteration
}
reminder_info->match = true;
return false; // stop iteration
}
bool reminder_db_find_by_timestamp_title(time_t timestamp, const char *title,
TimelineItemStorageFilterCallback filter_cb,
TimelineItem *reminder_out) {
PBL_ASSERTN(reminder_out);
ReminderInfo reminder_info = {
.filter_cb = filter_cb,
.timestamp = timestamp,
.title = title,
.reminder_out = reminder_out,
.match = false
};
timeline_item_storage_each(&s_storage, prv_timestamp_title_compare_func, &reminder_info);
return reminder_info.match;
}
static status_t prv_insert_reminder(const uint8_t *key, int key_len,
const uint8_t *val, int val_len, bool mark_synced) {
const SerializedTimelineItemHeader *hdr = (const SerializedTimelineItemHeader *)val;
const bool has_reminded = hdr->common.reminded;
status_t rv = timeline_item_storage_insert(&s_storage, key, key_len, val, val_len, mark_synced);
char uuid_buffer[UUID_STRING_BUFFER_LENGTH];
uuid_to_string((Uuid *)key, uuid_buffer);
PBL_LOG(LOG_LEVEL_INFO, "Reminder added: %s", uuid_buffer);
if (rv == S_SUCCESS) {
if (has_reminded) {
reminders_handle_reminder_updated(&hdr->common.id);
} else {
rv = reminders_update_timer();
}
}
return rv;
}
status_t reminder_db_insert_item(TimelineItem *item) {
if (item->header.type != TimelineItemTypeReminder) {
return E_INVALID_ARGUMENT;
}
size_t payload_size = timeline_item_get_serialized_payload_size(item);
uint8_t *buffer = kernel_malloc_check(sizeof(SerializedTimelineItemHeader) + payload_size);
timeline_item_serialize_header(item, (SerializedTimelineItemHeader *) buffer);
timeline_item_serialize_payload(item, buffer + sizeof(SerializedTimelineItemHeader),
payload_size);
// only for items without attributes as of right now
// Records inserted by the watch are dirty and need to be synced to the phone
const bool mark_synced = false;
status_t rv = prv_insert_reminder((uint8_t *)&item->header.id, sizeof(TimelineItemId),
buffer, sizeof(SerializedTimelineItemHeader) + payload_size, mark_synced);
blob_db_sync_record(BlobDBIdReminders, (uint8_t *)&item->header.id, sizeof(TimelineItemId),
rtc_get_time());
kernel_free(buffer);
return rv;
}
static status_t prv_reminder_db_delete_common(const uint8_t *key, int key_len) {
status_t rv = timeline_item_storage_delete(&s_storage, key, key_len);
if (rv == S_SUCCESS) {
reminders_update_timer();
}
return rv;
}
status_t reminder_db_delete_item(const TimelineItemId *id, bool send_event) {
return (send_event ? reminder_db_delete :
prv_reminder_db_delete_common)((uint8_t *)id, sizeof(TimelineItemId));
}
bool reminder_db_is_empty(void) {
return timeline_item_storage_is_empty(&s_storage);
}
status_t reminder_db_set_status_bits(const TimelineItemId *id, uint8_t status) {
return timeline_item_storage_set_status_bits(&s_storage, (uint8_t *)id,
sizeof(ReminderId), status);
}
/////////////////////////
// Blob DB API
/////////////////////////
void reminder_db_init(void) {
timeline_item_storage_init(&s_storage,
REMINDER_DB_FILE_NAME,
REMINDER_DB_MAX_SIZE,
MAX_REMINDER_AGE);
reminders_init();
}
void reminder_db_deinit(void) {
timeline_item_storage_deinit(&s_storage);
}
status_t reminder_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
analytics_inc(ANALYTICS_DEVICE_METRIC_REMINDER_RECEIVED_COUNT, AnalyticsClient_System);
// Records inserted from the phone are synced
const bool mark_synced = true;
return prv_insert_reminder(key, key_len, val, val_len, mark_synced);
}
int reminder_db_get_len(const uint8_t *key, int key_len) {
return timeline_item_storage_get_len(&s_storage, key, key_len);
}
status_t reminder_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len) {
return timeline_item_storage_read(&s_storage, key, key_len, val_out, val_out_len);
}
status_t reminder_db_delete(const uint8_t *key, int key_len) {
status_t rv = prv_reminder_db_delete_common(key, key_len);
reminders_handle_reminder_removed((Uuid *) key);
return rv;
}
status_t reminder_db_flush(void) {
return timeline_item_storage_flush(&s_storage);
}
status_t reminder_db_is_dirty(bool *is_dirty_out) {
*is_dirty_out = false;
return timeline_item_storage_each(&s_storage, sync_util_is_dirty_cb, is_dirty_out);
}
BlobDBDirtyItem* reminder_db_get_dirty_list(void) {
BlobDBDirtyItem *dirty_list = NULL;
timeline_item_storage_each(&s_storage, sync_util_build_dirty_list_cb, &dirty_list);
return dirty_list;
}
status_t reminder_db_mark_synced(const uint8_t *key, int key_len) {
PBL_LOG(LOG_LEVEL_DEBUG, "reminder_db_mark_synced");
return timeline_item_storage_mark_synced(&s_storage, key, key_len);
}

View file

@ -0,0 +1,93 @@
/*
* 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 "api.h"
#include "timeline_item_storage.h"
#include "system/status_codes.h"
#include "services/normal/timeline/item.h"
// reminderdb specific
//! Get the \ref TimelineItem with a given ID
//! @param item_out pointer to a TimelineItem to store the item into
//! @param id pointer to the UUID that corresponds to the \ref TimelineItem to find
//! @return \ref S_SUCCESS if the function succeeds, error code otherwise
status_t reminder_db_read_item(TimelineItem *item_out, TimelineItemId *id);
//! Get the header of the earliest earliest \ref TimelineItem in the reminderdb
//! @param next_item_out pointer to a \ref TimelineItem (header only, no attributes or actions)
//! which will be set to the earliest item in reminderdb
//! @return \ref S_NO_MORE_ITEMS if there are no items in reminderdb, S_SUCCESS on success,
//! error code otherwise
status_t reminder_db_next_item_header(TimelineItem *next_item_out);
//! Insert a timeline item into reminderdb
//! @param item pointer to item to insert into reminderdb
//! @return \ref S_SUCCESS if the function succeeds, error code otherwise
status_t reminder_db_insert_item(TimelineItem *item);
//! Delete an item from reminderdb by ID
//! @param id pointer to a \ref TimelineItemId to delete
//! @param send_event if true, send a PebbleReminderEvent after deletion
//! @return \ref S_SUCCESS if the function succeeds, error code otherwise
status_t reminder_db_delete_item(const TimelineItemId *id, bool send_event);
//! Delete every reminder in reminderdb that has a given parent
//! @param parent_id the uuid of the parent whose children we want to delete
//! @return \ref S_SUCCESS if all reminders were deleted, an error code otherwise
status_t reminder_db_delete_with_parent(const TimelineItemId *parent_id);
//! Check whether or not there are items in reminder db.
//! @return true if the DB is empty, false otherwise
bool reminder_db_is_empty(void);
status_t reminder_db_set_status_bits(const TimelineItemId *id, uint8_t status);
//! Finds a reminder that is identical to the specified one by first searching the timestamps,
//! then comparing the titles and lastly using the filter callback (if provided)
//! @param timestamp Time to match with
//! @param title Title to match with
//! @param filter Callback to custom filter function for additional checks
//! @param reminder_out Out param for matching reminder - cannot be null, only valid if the
//! function returns true
//! @return true if matching reminder found in storage, false otherwise
bool reminder_db_find_by_timestamp_title(time_t timestamp, const char *title,
TimelineItemStorageFilterCallback filter,
TimelineItem *reminder_out);
// blobdb functions
void reminder_db_init(void);
void reminder_db_deinit(void);
status_t reminder_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
int reminder_db_get_len(const uint8_t *key, int key_len);
status_t reminder_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
status_t reminder_db_delete(const uint8_t *key, int key_len);
status_t reminder_db_flush(void);
status_t reminder_db_is_dirty(bool *is_dirty_out);
BlobDBDirtyItem* reminder_db_get_dirty_list(void);
status_t reminder_db_mark_synced(const uint8_t *key, int key_len);

View file

@ -0,0 +1,241 @@
/*
* 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 "sync.h"
#include "endpoint.h"
#include "util.h"
#include "kernel/pbl_malloc.h"
#include "services/common/comm_session/session.h"
#include "services/common/system_task.h"
#include "system/logging.h"
#include "util/list.h"
#include <stdlib.h>
#define SYNC_TIMEOUT_SECONDS 30
static BlobDBSyncSession *s_sync_sessions = NULL;
static bool prv_session_id_filter_callback(ListNode *node, void *data) {
BlobDBId db_id = (BlobDBId)data;
BlobDBSyncSession *session = (BlobDBSyncSession *)node;
if (session->session_type == BlobDBSyncSessionTypeRecord) {
return false;
}
return session->db_id == db_id;
}
static bool prv_session_token_filter_callback(ListNode *node, void *data) {
uint16_t token = (uint16_t)(uintptr_t)data;
BlobDBSyncSession *session = (BlobDBSyncSession *)node;
return session->current_token == token;
}
static void prv_timeout_kernelbg_callback(void *data) {
PBL_LOG(LOG_LEVEL_INFO, "Blob DB Sync timeout");
BlobDBSyncSession *session = data;
blob_db_sync_cancel(session);
}
static void prv_timeout_timer_callback(void *data) {
system_task_add_callback(prv_timeout_kernelbg_callback, data);
}
static void prv_send_writeback(BlobDBSyncSession *session) {
// we want to write-back the first item in the dirty list
BlobDBDirtyItem *dirty_item = session->dirty_list;
int item_size = blob_db_get_len(session->db_id, dirty_item->key, dirty_item->key_len);
if (item_size == 0) {
// item got removed during the sync. Go to the next one
blob_db_sync_next(session);
return;
}
if (!comm_session_get_system_session()) {
PBL_LOG(LOG_LEVEL_INFO, "Cancelling sync: No route to phone");
blob_db_sync_cancel(session);
return;
}
// read item into a temporary buffer
void *item_buf = kernel_malloc_check(item_size);
status_t status = blob_db_read(session->db_id,
dirty_item->key,
dirty_item->key_len,
item_buf, item_size);
if (PASSED(status)) {
regular_timer_add_multisecond_callback(&session->timeout_timer, SYNC_TIMEOUT_SECONDS);
} else if (status == E_DOES_NOT_EXIST) {
// item was removed
blob_db_sync_next(session);
} else {
// something went terribly wrong
PBL_LOG(LOG_LEVEL_ERROR, "Failed to read blob DB during sync. Error code: 0x%"PRIx32, status);
blob_db_sync_cancel(session);
}
// only one writeback in flight at a time
session->state = BlobDBSyncSessionStateWaitingForAck;
if (session->session_type == BlobDBSyncSessionTypeDB) {
session->current_token = blob_db_endpoint_send_writeback(session->db_id,
dirty_item->last_updated,
dirty_item->key,
dirty_item->key_len,
item_buf,
item_size);
} else {
session->current_token = blob_db_endpoint_send_write(session->db_id,
dirty_item->last_updated,
dirty_item->key,
dirty_item->key_len,
item_buf,
item_size);
}
kernel_free(item_buf);
}
BlobDBSyncSession* prv_create_sync_session(BlobDBId db_id, BlobDBDirtyItem *dirty_list,
BlobDBSyncSessionType session_type) {
BlobDBSyncSession *session = kernel_zalloc_check(sizeof(BlobDBSyncSession));
session->state = BlobDBSyncSessionStateIdle;
session->db_id = db_id;
session->dirty_list = dirty_list;
session->session_type = session_type;
session->timeout_timer = (const RegularTimerInfo) {
.cb = prv_timeout_timer_callback,
.cb_data = session,
};
s_sync_sessions = (BlobDBSyncSession *)list_prepend((ListNode *)s_sync_sessions,
(ListNode *)session);
return session;
}
//! Will not return sessions for individual records
BlobDBSyncSession *blob_db_sync_get_session_for_id(BlobDBId db_id) {
return (BlobDBSyncSession *)list_find((ListNode *)s_sync_sessions,
prv_session_id_filter_callback,
(void *)(uintptr_t)db_id);
}
BlobDBSyncSession *blob_db_sync_get_session_for_token(uint16_t token) {
return (BlobDBSyncSession *)list_find((ListNode *)s_sync_sessions,
prv_session_token_filter_callback,
(void *)(uintptr_t)token);
}
status_t blob_db_sync_db(BlobDBId db_id) {
if (db_id >= NumBlobDBs) {
return E_INVALID_ARGUMENT;
}
PBL_LOG(LOG_LEVEL_INFO, "Starting BlobDB db sync: %d", db_id);
BlobDBDirtyItem *dirty_list = blob_db_get_dirty_list(db_id);
if (!dirty_list) {
blob_db_endpoint_send_sync_done(db_id);
return S_NO_ACTION_REQUIRED;
}
BlobDBSyncSession *session = blob_db_sync_get_session_for_id(db_id);
if (session) {
// already have a session in progress!
return E_BUSY;
}
session = prv_create_sync_session(db_id, dirty_list, BlobDBSyncSessionTypeDB);
prv_send_writeback(session);
return S_SUCCESS;
}
status_t blob_db_sync_record(BlobDBId db_id, const void *key, int key_len, time_t last_updated) {
if (db_id >= NumBlobDBs) {
return E_INVALID_ARGUMENT;
}
BlobDBSyncSession *session = blob_db_sync_get_session_for_id(db_id);
if (session) {
// This will get picked up by the current session when it is done with its dirty list
return S_SUCCESS;
}
char buffer[key_len + 1];
strncpy(buffer, (const char *)key, key_len);
buffer[key_len] = '\0';
PBL_LOG(LOG_LEVEL_INFO, "Starting BlobDB record sync: <%s>", buffer);
BlobDBDirtyItem *dirty_list = kernel_zalloc_check(sizeof(BlobDBDirtyItem) + key_len);
list_init((ListNode *)dirty_list);
dirty_list->last_updated = last_updated;
dirty_list->key_len = key_len;
memcpy(dirty_list->key, key, key_len);
session = prv_create_sync_session(db_id, dirty_list, BlobDBSyncSessionTypeRecord);
prv_send_writeback(session);
return S_SUCCESS;
}
void blob_db_sync_cancel(BlobDBSyncSession *session) {
PBL_LOG(LOG_LEVEL_DEBUG, "Cancelling session %d sync", session->db_id);
if (regular_timer_is_scheduled(&session->timeout_timer)) {
regular_timer_remove_callback(&session->timeout_timer);
}
blob_db_util_free_dirty_list(session->dirty_list);
list_remove((ListNode *)session, (ListNode **)&s_sync_sessions, NULL);
kernel_free(session);
}
void blob_db_sync_next(BlobDBSyncSession *session) {
PBL_LOG(LOG_LEVEL_DEBUG, "blob_db_sync_next");
BlobDBDirtyItem *dirty_item = session->dirty_list;
blob_db_mark_synced(session->db_id, dirty_item->key, dirty_item->key_len);
// we're done with this item, we pop it off the stack
list_remove((ListNode *)dirty_item, (ListNode **)&session->dirty_list, NULL);
kernel_free(dirty_item);
if (session->dirty_list) {
prv_send_writeback(session);
} else {
// Check if new records became dirty while syncing the current list
// New records could have been added while we were syncing OR
// the list could be incomplete because we ran out of memory
session->dirty_list = blob_db_get_dirty_list(session->db_id);
if (session->dirty_list) {
prv_send_writeback(session);
} else {
PBL_LOG(LOG_LEVEL_INFO, "Finished syncing db %d, session type: %d", session->db_id,
session->session_type);
if (regular_timer_is_scheduled(&session->timeout_timer)) {
regular_timer_remove_callback(&session->timeout_timer);
}
if (session->session_type == BlobDBSyncSessionTypeDB) {
// Only send the sync done when syncing an entire db
blob_db_endpoint_send_sync_done(session->db_id);
}
list_remove((ListNode *)session, (ListNode **)&s_sync_sessions, NULL);
kernel_free(session);
}
}
}

View file

@ -0,0 +1,66 @@
/*
* 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 "api.h"
#include "endpoint.h"
#include "services/common/regular_timer.h"
typedef enum {
BlobDBSyncSessionStateIdle = 0,
BlobDBSyncSessionStateWaitingForAck = 1,
} BlobDBSyncSessionState;
typedef enum {
BlobDBSyncSessionTypeDB,
BlobDBSyncSessionTypeRecord,
} BlobDBSyncSessionType;
typedef struct {
ListNode node;
BlobDBSyncSessionState state;
BlobDBId db_id;
BlobDBDirtyItem *dirty_list;
RegularTimerInfo timeout_timer;
BlobDBToken current_token;
BlobDBSyncSessionType session_type;
} BlobDBSyncSession;
//! Start sync-ing a blobdb.
//! @param db_id the BlobDBId of the database to sync
status_t blob_db_sync_db(BlobDBId db_id);
//! Start sync-ing a key within a blobdb.
//! @param db_id the BlobDBId of the database to sync
//! @param key the key to sync
//! @param key_len the length of the key to sync
status_t blob_db_sync_record(BlobDBId db_id, const void *key, int key_len, time_t last_updated);
//! Get the sync session for a given ID. Will NOT return sessions for individual records
//! returns NULL if no sync is in progress
BlobDBSyncSession *blob_db_sync_get_session_for_id(BlobDBId db_id);
//! Get the sync session currently waiting for a response with the given token
//! return NULL if no sync is in progress
BlobDBSyncSession *blob_db_sync_get_session_for_token(BlobDBToken token);
//! Mark current item as synced and sync the next one
void blob_db_sync_next(BlobDBSyncSession *session);
//! Cancel the sync in progress. Pending items will be synced next time.
void blob_db_sync_cancel(BlobDBSyncSession *session);

View file

@ -0,0 +1,56 @@
/*
* 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 "sync_util.h"
#include "kernel/pbl_malloc.h"
#include "system/logging.h"
// Caution: CommonTimelineItemHeader .flags & .status are stored inverted and not auto-restored
// by the underlying db API. If .flags or .status is used from a CommonTimelineItemHeader below,
// be very careful
bool sync_util_is_dirty_cb(SettingsFile *file, SettingsRecordInfo *info, void *context) {
// If there is a single dirty record, update the out bool to dirty and stop iterating
if (info->dirty) {
*((bool *)context) = true;
return false;
}
return true;
}
bool sync_util_build_dirty_list_cb(SettingsFile *file, SettingsRecordInfo *info, void *context) {
if (info->dirty) {
BlobDBDirtyItem *dirty_list = *(BlobDBDirtyItem **)context;
BlobDBDirtyItem *new_node = kernel_zalloc(sizeof(BlobDBDirtyItem) + info->key_len);
if (!new_node) {
PBL_LOG(LOG_LEVEL_WARNING, "Ran out of memory while building a dirty list");
return false;
}
new_node->last_updated = info->last_modified;
new_node->key_len = info->key_len;
info->get_key(file, new_node->key, new_node->key_len);
*(BlobDBDirtyItem **)context =
(BlobDBDirtyItem *)list_prepend((ListNode *) dirty_list, (ListNode *)new_node);
}
return true;
}

View file

@ -0,0 +1,32 @@
/*
* 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/normal/blob_db/api.h"
#include "services/normal/settings/settings_file.h"
// Caution: CommonTimelineItemHeader .flags & .status are stored inverted and not auto-restored
// by the underlying db API. If .flags or .status is used from a CommonTimelineItemHeader below,
// be very careful.
//! A settings file each callback which checks if the there are dirty records in the file
//! @param context The address of a bool which will get set
bool sync_util_is_dirty_cb(SettingsFile *file, SettingsRecordInfo *info, void *context);
//! A settings file each callback which builds a BlobDBDirtyItem list
//! @param context The address of an empty dirty list which will get built
bool sync_util_build_dirty_list_cb(SettingsFile *file, SettingsRecordInfo *info, void *context);

View file

@ -0,0 +1,435 @@
/*
* 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 "timeline_item_storage.h"
#include "kernel/pbl_malloc.h"
#include "services/normal/filesystem/pfs.h"
#include "services/normal/settings/settings_raw_iter.h"
#include "system/logging.h"
#define MAX_CHILDREN_PER_PIN 3
typedef struct {
Uuid parent_id;
Uuid children_ids[MAX_CHILDREN_PER_PIN];
int num_children;
bool find_all;
} FindChildrenInfo;
typedef struct {
time_t current;
time_t best;
Uuid id;
uint32_t max_age;
TimelineItemStorageFilterCallback filter_cb;
bool found;
} NextInfo;
typedef struct {
time_t earliest;
} GcInfo;
typedef struct {
bool empty;
} AnyInfo;
// callback for settings_file_each that finds the first item by timestamp
static bool prv_each_first_item(SettingsFile *file, SettingsRecordInfo *info,
void *context) {
if (info->val_len < (int)sizeof(SerializedTimelineItemHeader) ||
info->key_len != UUID_SIZE) { // deleted or malformed values
if (info->key_len != UUID_SIZE) {
PBL_LOG(LOG_LEVEL_WARNING, "Found reminder with invalid key size %d; ignoring.",
info->key_len);
}
return true;
}
SerializedTimelineItemHeader hdr;
info->get_val(file, &hdr, sizeof(SerializedTimelineItemHeader));
// Restore flags & status
hdr.common.flags = ~hdr.common.flags;
hdr.common.status = ~hdr.common.status;
NextInfo *next_info = (NextInfo *)context;
time_t timestamp = timeline_item_get_tz_timestamp(&hdr.common);
// check if filter callback exists, then check if we should keep the item.
bool filtered_out = false;
if (next_info->filter_cb && !next_info->filter_cb(&hdr, context)) {
filtered_out = true;
}
if (!filtered_out &&
(timestamp < next_info->best || !next_info->found) &&
(timestamp >= (int)(next_info->current - next_info->max_age))) {
next_info->found = true;
next_info->best = timestamp;
info->get_key(file, (uint8_t *)&next_info->id, sizeof(Uuid));
}
return true; // continue iterating
}
static bool prv_each_any_item(SettingsFile *file, SettingsRecordInfo *info, void *context) {
if (info->val_len < (int)sizeof(SerializedTimelineItemHeader) ||
info->key_len != UUID_SIZE) {
return true; // continue looking
}
AnyInfo *anyinfo = context;
anyinfo->empty = false;
return false; // we found a valid entry
}
static bool prv_each_find_children(SettingsFile *file, SettingsRecordInfo *info,
void *context) {
FindChildrenInfo *find_info = (FindChildrenInfo *)context;
if (info->val_len < (int)sizeof(SerializedTimelineItemHeader) ||
info->key_len != UUID_SIZE) {
// malformed values; deleted values have their lengths set to 0
if (info->key_len != UUID_SIZE) {
PBL_LOG(LOG_LEVEL_WARNING, "Found malformed item with invalid key/val sizes; ignoring.");
}
return true;
}
SerializedTimelineItemHeader hdr;
info->get_val(file, &hdr, sizeof(SerializedTimelineItemHeader));
// Restore flags & status
hdr.common.flags = ~hdr.common.flags;
hdr.common.status = ~hdr.common.status;
if (uuid_equal(&find_info->parent_id, &hdr.common.parent_id)) {
find_info->children_ids[find_info->num_children++] = hdr.common.id;
}
// continue iterating if more children are expected and we want them all
return ((find_info->num_children < MAX_CHILDREN_PER_PIN) && find_info->find_all);
}
///////////////////////////////////
// Public API
///////////////////////////////////
bool timeline_item_storage_is_empty(TimelineItemStorage *storage) {
bool rv = true;
mutex_lock(storage->mutex);
AnyInfo any_info = { .empty = true };
status_t status = settings_file_each(&storage->file, prv_each_any_item, &any_info);
if (status) {
goto cleanup;
}
rv = any_info.empty;
cleanup:
mutex_unlock(storage->mutex);
return rv;
}
status_t timeline_item_storage_next_item(TimelineItemStorage *storage, Uuid *id_out,
TimelineItemStorageFilterCallback filter_cb) {
mutex_lock(storage->mutex);
NextInfo next_info = {0};
next_info.current = rtc_get_time();
next_info.max_age = storage->max_item_age;
next_info.filter_cb = filter_cb;
status_t rv = settings_file_each(&storage->file, prv_each_first_item, &next_info);
if (rv) {
goto cleanup;
}
if (!next_info.found) {
rv = S_NO_MORE_ITEMS;
goto cleanup;
}
*id_out = next_info.id;
rv = S_SUCCESS;
cleanup:
mutex_unlock(storage->mutex);
return rv;
}
bool timeline_item_storage_exists_with_parent(TimelineItemStorage *storage, const Uuid *parent_id) {
mutex_lock(storage->mutex);
FindChildrenInfo info = {
.parent_id = *parent_id,
.num_children = 0,
.find_all = false,
};
status_t rv = settings_file_each(&storage->file, prv_each_find_children, &info);
if (rv) {
goto cleanup;
}
if (info.num_children) {
rv = S_SUCCESS;
} else {
rv = S_NO_MORE_ITEMS;
}
cleanup:
mutex_unlock(storage->mutex);
return rv == S_SUCCESS;
}
status_t timeline_item_storage_delete_with_parent(
TimelineItemStorage *storage,
const Uuid *parent_id,
TimelineItemStorageChildDeleteCallback child_delete_cb) {
mutex_lock(storage->mutex);
FindChildrenInfo info = {
.parent_id = *parent_id,
.num_children = 0,
.find_all = true,
};
status_t rv = settings_file_each(&storage->file, prv_each_find_children, &info);
if (rv) {
goto cleanup;
}
for (int i = 0; i < info.num_children; ++i) {
const void *key = &info.children_ids[i];
rv = settings_file_delete(&storage->file, key, sizeof(Uuid));
if (rv != S_SUCCESS) {
goto cleanup;
}
if (child_delete_cb) {
child_delete_cb((Uuid *)key);
}
}
cleanup:
mutex_unlock(storage->mutex);
return rv;
}
//! Caution: CommonTimelineItemHeader .flags & .status are stored inverted and not auto-restored
status_t timeline_item_storage_each(TimelineItemStorage *storage,
TimelineItemStorageEachCallback each, void *data) {
mutex_lock(storage->mutex);
status_t rv = settings_file_each(&storage->file, each, data);
mutex_unlock(storage->mutex);
return rv;
}
void timeline_item_storage_init(TimelineItemStorage *storage,
char *filename, uint32_t max_size, uint32_t max_age) {
*storage = (TimelineItemStorage){
.name = filename,
.max_size = max_size,
.max_item_age = max_age,
.mutex = mutex_create(),
};
status_t rv = settings_file_open(&storage->file, storage->name, storage->max_size);
if (FAILED(rv)) {
PBL_LOG(LOG_LEVEL_ERROR, "Unable to create settings file %s, rv = %"PRId32 "!",
filename, rv);
}
}
void timeline_item_storage_deinit(TimelineItemStorage *storage) {
settings_file_close(&storage->file);
}
status_t timeline_item_storage_insert(TimelineItemStorage *storage,
const uint8_t *key, int key_len, const uint8_t *val, int val_len, bool mark_as_synced) {
if (key_len != UUID_SIZE ||
val_len > SETTINGS_VAL_MAX_LEN ||
val_len < (int)sizeof(SerializedTimelineItemHeader)) {
return E_INVALID_ARGUMENT;
}
// Check that the layout has the correct items
if (!timeline_item_verify_layout_serialized(val, val_len)) {
PBL_LOG(LOG_LEVEL_WARNING, "Timeline item does not have the correct attributes");
return E_INVALID_ARGUMENT;
}
SerializedTimelineItemHeader *hdr = (SerializedTimelineItemHeader *)val;
// verify that the item isn't too old
time_t now = rtc_get_time();
time_t timestamp = timeline_item_get_tz_timestamp(&hdr->common);
time_t end_timestamp = timestamp + hdr->common.duration * SECONDS_PER_MINUTE;
if (end_timestamp < (int)(now - storage->max_item_age)) {
PBL_LOG(LOG_LEVEL_WARNING, "Rejecting stale timeline item %ld seconds old",
now - timestamp);
return E_INVALID_OPERATION;
}
// FIXME: PBL-39523 timeline_item_storage_insert modifies a const buffer
// Invert flags & status to store on flash
hdr->common.flags = ~hdr->common.flags;
hdr->common.status = ~hdr->common.status;
mutex_lock(storage->mutex);
status_t rv = settings_file_set(&storage->file, key, key_len, val, val_len);
// Restore flags & status
hdr->common.flags = ~hdr->common.flags;
hdr->common.status = ~hdr->common.status;
if (mark_as_synced) {
settings_file_mark_synced(&storage->file, key, key_len);
}
mutex_unlock(storage->mutex);
return rv;
}
int timeline_item_storage_get_len(TimelineItemStorage *storage,
const uint8_t *key, int key_len) {
mutex_lock(storage->mutex);
status_t rv = settings_file_get_len(&storage->file, key, key_len);
mutex_unlock(storage->mutex);
return rv;
}
status_t timeline_item_storage_read(TimelineItemStorage *storage,
const uint8_t *key, int key_len, uint8_t *val_out, int val_len) {
if (key_len != UUID_SIZE) {
return E_INVALID_ARGUMENT;
}
mutex_lock(storage->mutex);
status_t rv = settings_file_get(&storage->file, key, key_len, val_out, val_len);
// Restore flags & status
SerializedTimelineItemHeader *hdr = (SerializedTimelineItemHeader *)val_out;
hdr->common.flags = ~hdr->common.flags;
hdr->common.status = ~hdr->common.status;
mutex_unlock(storage->mutex);
return rv;
}
status_t timeline_item_storage_get_from_settings_record(SettingsFile *file,
SettingsRecordInfo *info,
TimelineItem *item) {
uint8_t *read_buf = kernel_zalloc_check(info->val_len);
info->get_val(file, read_buf, info->val_len);
SerializedTimelineItemHeader *header = (SerializedTimelineItemHeader *)read_buf;
// Restore flags & status
header->common.flags = ~header->common.flags;
header->common.status = ~header->common.status;
uint8_t *payload = read_buf + sizeof(SerializedTimelineItemHeader);
status_t rv = timeline_item_deserialize_item(item, header, payload) ? S_SUCCESS : E_INTERNAL;
kernel_free(read_buf);
return rv;
}
status_t timeline_item_storage_set_status_bits(TimelineItemStorage *storage,
const uint8_t *key, int key_len, uint8_t status) {
if (key_len != UUID_SIZE) {
return E_INVALID_ARGUMENT;
}
mutex_lock(storage->mutex);
int offset = offsetof(SerializedTimelineItemHeader, common.status);
// Invert status to store on flash
status = ~status;
status_t rv = settings_file_set_byte(&storage->file, key, key_len, offset, status);
mutex_unlock(storage->mutex);
return rv;
}
status_t timeline_item_storage_delete(TimelineItemStorage *storage,
const uint8_t *key, int key_len) {
if (key_len != UUID_SIZE) {
return E_INVALID_ARGUMENT;
}
mutex_lock(storage->mutex);
status_t rv = settings_file_delete(&storage->file, key, key_len);
mutex_unlock(storage->mutex);
return rv;
}
status_t timeline_item_storage_mark_synced(TimelineItemStorage *storage,
const uint8_t *key, int key_len) {
if (key_len == 0) {
return E_INVALID_ARGUMENT;
}
mutex_lock(storage->mutex);
status_t rv = settings_file_mark_synced(&storage->file, key, key_len);
mutex_unlock(storage->mutex);
return rv;
}
static void prv_flush_rewrite_cb(SettingsFile *old,
SettingsFile *new,
SettingsRecordInfo *info,
void *context) {
if ((unsigned)info->key_len != sizeof(Uuid) ||
(unsigned)info->val_len < sizeof(SerializedTimelineItemHeader)) {
// invalid
return;
}
SerializedTimelineItemHeader hdr;
info->get_val(old, &hdr, sizeof(SerializedTimelineItemHeader));
// Restore flags & status
hdr.common.flags = ~hdr.common.flags;
hdr.common.status = ~hdr.common.status;
// keep watch-only items
if (hdr.common.flags & TimelineItemFlagFromWatch) {
// fetch the whole item
uint8_t *val = kernel_malloc_check(info->val_len);
uint8_t *key = kernel_malloc_check(info->key_len);
info->get_val(old, val, info->val_len);
info->get_key(old, key, info->key_len);
// Don't restore flags & status here - we're writing it back immediately.
// write it to the new file
settings_file_set(new, key, info->key_len, val, info->val_len);
kernel_free(key);
kernel_free(val);
}
}
status_t timeline_item_storage_flush(TimelineItemStorage *storage) {
mutex_lock(storage->mutex);
status_t rv = settings_file_rewrite(&storage->file, prv_flush_rewrite_cb, NULL);
mutex_unlock(storage->mutex);
return rv;
}

View file

@ -0,0 +1,89 @@
/*
* 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"
#include "services/common/regular_timer.h"
#include "services/normal/settings/settings_file.h"
#include "os/mutex.h"
#include "services/normal/timeline/item.h"
typedef struct {
SettingsFile file;
PebbleMutex *mutex;
char *name;
size_t max_size;
uint32_t max_item_age; // seconds
} TimelineItemStorage;
typedef bool (*TimelineItemStorageFilterCallback)(SerializedTimelineItemHeader *hdr,
void *context);
typedef SettingsFileEachCallback TimelineItemStorageEachCallback;
//! Callback used for timeline_item_storage_delete_with_parent.
//! Called with the UUID of each child that is deleted.
typedef void (*TimelineItemStorageChildDeleteCallback)(const Uuid *id);
void timeline_item_storage_init(TimelineItemStorage *storage,
char *filename, uint32_t max_size, uint32_t max_age);
void timeline_item_storage_deinit(TimelineItemStorage *storage);
bool timeline_item_storage_exists_with_parent(TimelineItemStorage *storage, const Uuid *parent_id);
status_t timeline_item_storage_flush(TimelineItemStorage *storage);
status_t timeline_item_storage_delete(TimelineItemStorage *storage,
const uint8_t *key, int key_len);
status_t timeline_item_storage_read(TimelineItemStorage *storage,
const uint8_t *key, int key_len, uint8_t *val_out, int val_len);
// This temporarily allocates heap memory, so use sparingly to prevent heap fragmentation
status_t timeline_item_storage_get_from_settings_record(SettingsFile *file,
SettingsRecordInfo *info,
TimelineItem *item);
status_t timeline_item_storage_set_status_bits(TimelineItemStorage *storage,
const uint8_t *key, int key_len, uint8_t status);
int timeline_item_storage_get_len(TimelineItemStorage *storage,
const uint8_t *key, int key_len);
status_t timeline_item_storage_insert(TimelineItemStorage *storage,
const uint8_t *key, int key_len, const uint8_t *val, int val_len, bool mark_as_synced);
//! Caution: CommonTimelineItemHeader .flags & .status are stored inverted and not auto-restored
status_t timeline_item_storage_each(TimelineItemStorage *storage,
TimelineItemStorageEachCallback each, void *data);
status_t timeline_item_storage_mark_synced(TimelineItemStorage *storage,
const uint8_t *key, int key_len);
status_t timeline_item_storage_delete_with_parent(
TimelineItemStorage *storage,
const Uuid *parent_id,
TimelineItemStorageChildDeleteCallback child_delete_cb);
//! filter_cb is a type that returns TRUE if the item should be used, or FALSE if the item should
//! be ignored.
status_t timeline_item_storage_next_item(TimelineItemStorage *storage, Uuid *id_out,
TimelineItemStorageFilterCallback filter_cb);
bool timeline_item_storage_is_empty(TimelineItemStorage *storage);

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.
*/
#include "util.h"
#include "kernel/pbl_malloc.h"
#include <stdlib.h>
void blob_db_util_free_dirty_list(BlobDBDirtyItem *dirty_list) {
ListNode *head = &dirty_list->node;
ListNode *cur;
while (head) {
cur = head;
list_remove(cur, &head, NULL);
kernel_free(cur);
}
}

View file

@ -0,0 +1,21 @@
/*
* 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 "api.h"
void blob_db_util_free_dirty_list(BlobDBDirtyItem *dirty_list);

View file

@ -0,0 +1,230 @@
/*
* 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 "watch_app_prefs_db.h"
#include "kernel/pbl_malloc.h"
#include "os/mutex.h"
#include "services/normal/filesystem/pfs.h"
#include "services/normal/settings/settings_file.h"
#include "services/normal/weather/weather_service_private.h"
#include "system/logging.h"
#include "system/status_codes.h"
#include "util/units.h"
#include "util/uuid.h"
static struct {
SettingsFile settings_file;
PebbleRecursiveMutex *mutex;
// We cache the reminder app prefs because they're read in reminder_app_get_info() which needs to
// be fast because it's called by analytics from the system task while counting timeline pins
bool is_cached_reminder_app_prefs_valid;
SerializedReminderAppPrefs cached_reminder_app_prefs;
} s_watch_app_prefs_db;
#define SETTINGS_FILE_NAME "watch_app_prefs"
#define SETTINGS_FILE_SIZE KiBYTES(20)
T_STATIC const char *PREF_KEY_SEND_TEXT_APP = "sendTextApp";
// Settings helpers
////////////////////////////////////////////////////////////////////////////////
static status_t prv_lock_mutex_and_open_file(void) {
mutex_lock_recursive(s_watch_app_prefs_db.mutex);
status_t rv = settings_file_open(&s_watch_app_prefs_db.settings_file,
SETTINGS_FILE_NAME,
SETTINGS_FILE_SIZE);
if (rv != S_SUCCESS) {
mutex_unlock_recursive(s_watch_app_prefs_db.mutex);
}
return rv;
}
static void prv_close_file_and_unlock_mutex(void) {
settings_file_close(&s_watch_app_prefs_db.settings_file);
mutex_unlock_recursive(s_watch_app_prefs_db.mutex);
}
// WatchAppPrefDB API
////////////////////////////////////////////////////////////////////////////////
static void *prv_get_prefs(const char *pref_key) {
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return NULL;
}
const int len = settings_file_get_len(&s_watch_app_prefs_db.settings_file, pref_key,
strlen(pref_key));
void *prefs = task_zalloc(len);
if (prefs) {
rv = settings_file_get(&s_watch_app_prefs_db.settings_file, pref_key,
strlen(pref_key), prefs, len);
if (rv != S_SUCCESS) {
task_free(prefs);
prefs = NULL;
}
}
prv_close_file_and_unlock_mutex();
return prefs;
}
SerializedSendTextPrefs *watch_app_prefs_get_send_text(void) {
return (SerializedSendTextPrefs *)prv_get_prefs(PREF_KEY_SEND_TEXT_APP);
}
SerializedWeatherAppPrefs *watch_app_prefs_get_weather(void) {
return (SerializedWeatherAppPrefs *)prv_get_prefs(PREF_KEY_WEATHER_APP);
}
SerializedReminderAppPrefs *watch_app_prefs_get_reminder(void) {
SerializedReminderAppPrefs *result = NULL;
mutex_lock_recursive(s_watch_app_prefs_db.mutex);
if (s_watch_app_prefs_db.is_cached_reminder_app_prefs_valid) {
result = task_zalloc(sizeof(*result));
if (result) {
*result = s_watch_app_prefs_db.cached_reminder_app_prefs;
}
} else {
result = prv_get_prefs(PREF_KEY_REMINDER_APP);
s_watch_app_prefs_db.is_cached_reminder_app_prefs_valid = true;
s_watch_app_prefs_db.cached_reminder_app_prefs = result ? *result :
(SerializedReminderAppPrefs) {};
}
mutex_unlock_recursive(s_watch_app_prefs_db.mutex);
return result;
}
void watch_app_prefs_destroy_weather(SerializedWeatherAppPrefs *prefs) {
if (prefs) {
task_free(prefs);
}
}
// BlobDB APIs
////////////////////////////////////////////////////////////////////////////////
void watch_app_prefs_db_init(void) {
s_watch_app_prefs_db.mutex = mutex_create_recursive();
}
// All entries in this db currently follow a structure of base data + arbitrary list of records.
// All records in the list are the same size
static bool prv_validate_received_pref(const size_t received_val_size, const size_t min_val_size,
const size_t num_records, const size_t record_size) {
if ((received_val_size % record_size) != min_val_size) {
return false;
}
const size_t calculated_size = min_val_size + (num_records * record_size);
return received_val_size >= calculated_size;
}
static bool prv_is_key_valid(const uint8_t *received_key, size_t received_key_len,
const char *system_key) {
return (received_key_len == strlen(system_key)) &&
(memcmp(received_key, system_key, received_key_len) == 0);
}
status_t watch_app_prefs_db_insert(const uint8_t *key, int key_len, const uint8_t *val,
int val_len) {
const bool is_valid_send_text_key = prv_is_key_valid(key, key_len, PREF_KEY_SEND_TEXT_APP);
const bool is_valid_weather_key = prv_is_key_valid(key, key_len, PREF_KEY_WEATHER_APP);
const bool is_valid_reminder_key = prv_is_key_valid(key, key_len, PREF_KEY_REMINDER_APP);
if (!is_valid_send_text_key && !is_valid_weather_key && !is_valid_reminder_key) {
PBL_LOG(LOG_LEVEL_ERROR, "Error inserting app_prefs: invalid key");
return E_INVALID_ARGUMENT;
}
if (is_valid_send_text_key &&
!prv_validate_received_pref(val_len, sizeof(SerializedSendTextPrefs),
((SerializedSendTextPrefs *)val)->num_contacts,
sizeof(SerializedSendTextContact))) {
PBL_LOG(LOG_LEVEL_ERROR, "Error inserting app_prefs: invalid send text contact list");
return E_INVALID_ARGUMENT;
}
if (is_valid_weather_key &&
!prv_validate_received_pref(val_len, sizeof(SerializedWeatherAppPrefs),
((SerializedWeatherAppPrefs *)val)->num_locations,
sizeof(Uuid))) {
PBL_LOG(LOG_LEVEL_ERROR, "Error inserting app_prefs: invalid weather list");
return E_INVALID_ARGUMENT;
}
status_t rv = prv_lock_mutex_and_open_file();
if (rv == S_SUCCESS) {
rv = settings_file_set(&s_watch_app_prefs_db.settings_file, key, key_len, val, val_len);
// Cache the data we just set if it was for the Reminders app
const int expected_reminder_app_prefs_size = sizeof(SerializedReminderAppPrefs);
if ((rv == S_SUCCESS) &&
is_valid_reminder_key &&
(val_len == expected_reminder_app_prefs_size)) {
s_watch_app_prefs_db.is_cached_reminder_app_prefs_valid = true;
memcpy(&s_watch_app_prefs_db.cached_reminder_app_prefs, val,
(size_t)expected_reminder_app_prefs_size);
}
prv_close_file_and_unlock_mutex();
}
return rv;
}
int watch_app_prefs_db_get_len(const uint8_t *key, int key_len) {
int len = 0;
if (prv_lock_mutex_and_open_file() == S_SUCCESS) {
len = settings_file_get_len(&s_watch_app_prefs_db.settings_file, key, key_len);
prv_close_file_and_unlock_mutex();
}
return len;
}
status_t watch_app_prefs_db_read(const uint8_t *key, int key_len, uint8_t *val_out,
int val_out_len) {
if (!val_out) {
return E_INVALID_ARGUMENT;
}
status_t rv = prv_lock_mutex_and_open_file();
if (rv == S_SUCCESS) {
rv = settings_file_get(&s_watch_app_prefs_db.settings_file, key, key_len, val_out, val_out_len);
prv_close_file_and_unlock_mutex();
}
return rv;
}
status_t watch_app_prefs_db_delete(const uint8_t *key, int key_len) {
status_t rv = prv_lock_mutex_and_open_file();
if (rv == S_SUCCESS) {
rv = settings_file_delete(&s_watch_app_prefs_db.settings_file, key, key_len);
prv_close_file_and_unlock_mutex();
}
return rv;
}
status_t watch_app_prefs_db_flush(void) {
mutex_lock_recursive(s_watch_app_prefs_db.mutex);
status_t rv = pfs_remove(SETTINGS_FILE_NAME);
mutex_unlock_recursive(s_watch_app_prefs_db.mutex);
return rv;
}

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 "apps/system_apps/send_text/send_text_app_prefs.h"
#include "apps/system_apps/reminders/reminder_app_prefs.h"
#include "services/normal/weather/weather_service_private.h"
#include "system/status_codes.h"
// Reads the Send Text app prefs from the db
// @return Pointer to new \ref SerializedSendTextPrefs, NULL on failure
// @note task_free() must be called on the pointer when done with the memory
SerializedSendTextPrefs *watch_app_prefs_get_send_text(void);
// Reads the Weather app location ordering from the db
// @return pointer to new \ref SerializedWeatherAppPrefs, NULL on failure
// @note use weather_app_prefs_destroy_weather() to free memory allocated by this method
SerializedWeatherAppPrefs *watch_app_prefs_get_weather(void);
// Reads the Reminder App prefs from the db
// @return pointer to new \ref SerializedRemindersAppPrefs, NULL on failure
// @note task_free() must be called on the pointer when done with the memory
SerializedReminderAppPrefs *watch_app_prefs_get_reminder(void);
// Frees memory allocated from watch_app_prefs_get_weather()
void watch_app_prefs_destroy_weather(SerializedWeatherAppPrefs *prefs);
///////////////////////////////////////////
// BlobDB Boilerplate (see blob_db/api.h)
///////////////////////////////////////////
void watch_app_prefs_db_init(void);
status_t watch_app_prefs_db_insert(const uint8_t *key, int key_len, const uint8_t *val,
int val_len);
int watch_app_prefs_db_get_len(const uint8_t *key, int key_len);
status_t watch_app_prefs_db_read(const uint8_t *key, int key_len, uint8_t *val_out,
int val_out_len);
status_t watch_app_prefs_db_delete(const uint8_t *key, int key_len);
status_t watch_app_prefs_db_flush(void);

View file

@ -0,0 +1,290 @@
/*
* 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 "weather_db.h"
#include "kernel/pbl_malloc.h"
#include "os/mutex.h"
#include "services/normal/filesystem/pfs.h"
#include "services/normal/settings/settings_file.h"
#include "services/normal/weather/weather_service.h"
#include "services/normal/weather/weather_types.h"
#include "system/passert.h"
#include "util/units.h"
#define SETTINGS_FILE_NAME "weatherdb"
#define SETTINGS_FILE_SIZE (KiBYTES(30))
static struct {
SettingsFile settings_file;
PebbleMutex *mutex;
} s_weather_db;
typedef struct WeatherDBIteratorData {
WeatherDBIteratorCallback cb;
void *cb_ctx;
} WeatherDBIteratorData;
///////////////////////////
// Weather DB API
///////////////////////////
static status_t prv_lock_mutex_and_open_file(void) {
mutex_lock(s_weather_db.mutex);
status_t rv = settings_file_open(&s_weather_db.settings_file,
SETTINGS_FILE_NAME,
SETTINGS_FILE_SIZE);
if (rv != S_SUCCESS) {
mutex_unlock(s_weather_db.mutex);
}
return rv;
}
static void prv_close_file_and_unlock_mutex(void) {
settings_file_close(&s_weather_db.settings_file);
mutex_unlock(s_weather_db.mutex);
}
static bool prv_weather_db_for_each_cb(SettingsFile *file, SettingsRecordInfo *info,
void *context) {
if ((info->val_len == 0) || (info->key_len != sizeof(WeatherDBKey))) {
return true;
}
WeatherDBKey key;
info->get_key(file, &key, info->key_len);
WeatherDBEntry *entry = task_zalloc_check(info->val_len);
info->get_val(file, entry, info->val_len);
if (entry->version != WEATHER_DB_CURRENT_VERSION) {
PBL_LOG(LOG_LEVEL_WARNING, "Version mismatch! Entry version: %" PRIu8 ", WeatherDB version: %u",
entry->version, WEATHER_DB_CURRENT_VERSION);
goto cleanup;
}
const WeatherDBIteratorData *cb_data = context;
cb_data->cb(&key, entry, cb_data->cb_ctx);
cleanup:
task_free(entry);
return true;
}
status_t weather_db_for_each(WeatherDBIteratorCallback callback, void *context) {
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
WeatherDBIteratorData data = (WeatherDBIteratorData) {
.cb = callback,
.cb_ctx = context
};
settings_file_each(&s_weather_db.settings_file,
prv_weather_db_for_each_cb,
&data);
prv_close_file_and_unlock_mutex();
return S_SUCCESS;
}
/////////////////////////
// Blob DB API
/////////////////////////
void weather_db_init(void) {
memset(&s_weather_db, 0, sizeof(s_weather_db));
s_weather_db.mutex = mutex_create();
}
status_t weather_db_flush(void) {
if (!weather_service_supported_by_phone()) {
// return E_RANGE, so the phone receives BLOB_DB_INVALID_DATABASE_ID and stops sending
// unwelcome weather records
return E_RANGE;
}
mutex_lock(s_weather_db.mutex);
pfs_remove(SETTINGS_FILE_NAME);
mutex_unlock(s_weather_db.mutex);
return S_SUCCESS;
}
status_t weather_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
if (!weather_service_supported_by_phone()) {
return E_RANGE;
}
if (key_len != sizeof(WeatherDBKey) ||
val_len < (int) MIN_ENTRY_SIZE ||
val_len > (int) MAX_ENTRY_SIZE) {
return E_INVALID_ARGUMENT;
}
const WeatherDBEntry *entry = (WeatherDBEntry *)val;
if (entry->version != WEATHER_DB_CURRENT_VERSION) {
PBL_LOG(LOG_LEVEL_WARNING,
"Version mismatch on insert! Entry version: %" PRIu8 ", WeatherDB version: %u",
entry->version, WEATHER_DB_CURRENT_VERSION);
return E_INVALID_ARGUMENT;
}
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_set(&s_weather_db.settings_file, key, key_len, val, val_len);
prv_close_file_and_unlock_mutex();
return rv;
}
int weather_db_get_len(const uint8_t *key, int key_len) {
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return 0;
}
PBL_ASSERTN(key_len == sizeof(WeatherDBKey));
int entry_len = settings_file_get_len(&s_weather_db.settings_file, key, key_len);
prv_close_file_and_unlock_mutex();
return entry_len;
}
status_t weather_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len) {
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
PBL_ASSERTN(key_len == sizeof(WeatherDBKey));
rv = settings_file_get(&s_weather_db.settings_file, key, key_len, val_out, val_out_len);
if (((WeatherDBEntry*)val_out)->version != WEATHER_DB_CURRENT_VERSION) {
// We might as well clear out the stale entry
PBL_LOG(LOG_LEVEL_WARNING, "Read an old weather DB entry");
settings_file_delete(&s_weather_db.settings_file, key, key_len);
rv = E_DOES_NOT_EXIST;
}
prv_close_file_and_unlock_mutex();
return rv;
}
status_t weather_db_delete(const uint8_t *key, int key_len) {
if (!weather_service_supported_by_phone()) {
return E_RANGE;
}
if (key_len != sizeof(WeatherDBKey)) {
return E_INVALID_ARGUMENT;
}
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
if (!settings_file_exists(&s_weather_db.settings_file, key, key_len)) {
prv_close_file_and_unlock_mutex();
return E_DOES_NOT_EXIST;
}
rv = settings_file_delete(&s_weather_db.settings_file, key, key_len);
prv_close_file_and_unlock_mutex();
return rv;
}
//-----------------------------------------------------------------------------
// Testing code only
#if UNITTEST
// SettingsFile Helpers
typedef struct {
uint16_t key_count;
WeatherDBKey *keys;
} SettingsFileEachKeyHelper;
static bool prv_each_inspect_keys(SettingsFile *file, SettingsRecordInfo *info, void *context) {
if ((info->val_len == 0) || (info->key_len != sizeof(WeatherDBKey))) {
// Invalid key, continue iterating
return true;
}
SettingsFileEachKeyHelper *key_helper = context;
if (key_helper->keys != NULL) {
info->get_key(file, (uint8_t *)&key_helper->keys[key_helper->key_count], sizeof(WeatherDBKey));
}
key_helper->key_count++;
// Continue iterating
return true;
}
status_t weather_db_get_num_keys(uint16_t *val_out) {
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
SettingsFileEachKeyHelper key_helper = {
.key_count = 0,
.keys = NULL,
};
settings_file_each(&s_weather_db.settings_file, prv_each_inspect_keys, &key_helper);
*val_out = key_helper.key_count;
prv_close_file_and_unlock_mutex();
return S_SUCCESS;
}
status_t weather_db_get_keys(WeatherDBKey *keys) {
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
SettingsFileEachKeyHelper key_helper = {
.key_count = 0,
.keys = keys,
};
settings_file_each(&s_weather_db.settings_file, prv_each_inspect_keys, &key_helper);
prv_close_file_and_unlock_mutex();
return S_SUCCESS;
}
status_t weather_db_insert_stale(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
// Quick and dirty insert which doesn't do any error checking. Used to insert stale entries
// for testing
status_t rv = prv_lock_mutex_and_open_file();
if (rv != S_SUCCESS) {
return rv;
}
rv = settings_file_set(&s_weather_db.settings_file, key, key_len, val, val_len);
prv_close_file_and_unlock_mutex();
return rv;
}
#endif

View file

@ -0,0 +1,79 @@
/*
* 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/normal/weather/weather_service.h"
#include "services/normal/weather/weather_types.h"
#include "system/status_codes.h"
#include "util/attributes.h"
#include "util/pstring.h"
#include "util/time/time.h"
#include "util/uuid.h"
#include <stdint.h>
#define WEATHER_DB_CURRENT_VERSION (3)
typedef Uuid WeatherDBKey;
typedef struct PACKED {
uint8_t version;
int16_t current_temp;
WeatherType current_weather_type;
int16_t today_high_temp;
int16_t today_low_temp;
WeatherType tomorrow_weather_type;
int16_t tomorrow_high_temp;
int16_t tomorrow_low_temp;
time_t last_update_time_utc;
bool is_current_location;
SerializedArray pstring16s;
} WeatherDBEntry;
typedef enum WeatherDbStringIndex {
WeatherDbStringIndex_LocationName,
WeatherDbStringIndex_ShortPhrase,
WeatherDbStringIndexCount,
} WeatherDbStringIndex;
#define MIN_ENTRY_SIZE (sizeof(WeatherDBEntry))
#define MAX_ENTRY_SIZE (MIN_ENTRY_SIZE + \
WEATHER_SERVICE_MAX_WEATHER_LOCATION_BUFFER_SIZE + \
WEATHER_SERVICE_MAX_SHORT_PHRASE_BUFFER_SIZE)
// Memory ownership: pointer to key and entry must not be saved, as they become invalid after
// the callback finishes
typedef void (*WeatherDBIteratorCallback)(WeatherDBKey *key, WeatherDBEntry *entry, void *context);
// ------------------------------------------------------------------------------------
// WeatherDB functions
status_t weather_db_for_each(WeatherDBIteratorCallback cb, void *context);
// ------------------------------------------------------------------------------------
// BlobDB Implementation
void weather_db_init(void);
status_t weather_db_flush(void);
status_t weather_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
int weather_db_get_len(const uint8_t *key, int key_len);
status_t weather_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
status_t weather_db_delete(const uint8_t *key, int key_len);