mirror of
https://github.com/google/pebble.git
synced 2025-07-05 06:10:27 -04:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
328
src/fw/services/normal/blob_db/api.c
Normal file
328
src/fw/services/normal/blob_db/api.c
Normal 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;
|
||||
}
|
186
src/fw/services/normal/blob_db/api.h
Normal file
186
src/fw/services/normal/blob_db/api.h
Normal 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);
|
23
src/fw/services/normal/blob_db/api_types.h
Normal file
23
src/fw/services/normal/blob_db/api_types.h
Normal 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;
|
424
src/fw/services/normal/blob_db/app_db.c
Normal file
424
src/fw/services/normal/blob_db/app_db.c
Normal 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;
|
||||
}
|
76
src/fw/services/normal/blob_db/app_db.h
Normal file
76
src/fw/services/normal/blob_db/app_db.h
Normal 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);
|
860
src/fw/services/normal/blob_db/app_glance_db.c
Normal file
860
src/fw/services/normal/blob_db/app_glance_db.c
Normal 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 = ¤t_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 = ¤t_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
|
51
src/fw/services/normal/blob_db/app_glance_db.h
Normal file
51
src/fw/services/normal/blob_db/app_glance_db.h
Normal 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);
|
58
src/fw/services/normal/blob_db/app_glance_db_private.h
Normal file
58
src/fw/services/normal/blob_db/app_glance_db_private.h
Normal 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)
|
183
src/fw/services/normal/blob_db/contacts_db.c
Normal file
183
src/fw/services/normal/blob_db/contacts_db.c
Normal 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;
|
||||
}
|
57
src/fw/services/normal/blob_db/contacts_db.h
Normal file
57
src/fw/services/normal/blob_db/contacts_db.h
Normal 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);
|
299
src/fw/services/normal/blob_db/endpoint.c
Normal file
299
src/fw/services/normal/blob_db/endpoint.c
Normal 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;
|
||||
}
|
40
src/fw/services/normal/blob_db/endpoint.h
Normal file
40
src/fw/services/normal/blob_db/endpoint.h
Normal 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);
|
354
src/fw/services/normal/blob_db/endpoint2.c
Normal file
354
src/fw/services/normal/blob_db/endpoint2.c
Normal 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;
|
||||
}
|
39
src/fw/services/normal/blob_db/endpoint_private.c
Normal file
39
src/fw/services/normal/blob_db/endpoint_private.c
Normal 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;
|
||||
}
|
72
src/fw/services/normal/blob_db/endpoint_private.h
Normal file
72
src/fw/services/normal/blob_db/endpoint_private.h
Normal 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);
|
437
src/fw/services/normal/blob_db/health_db.c
Normal file
437
src/fw/services/normal/blob_db/health_db.c
Normal 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;
|
||||
}
|
||||
|
60
src/fw/services/normal/blob_db/health_db.h
Normal file
60
src/fw/services/normal/blob_db/health_db.h
Normal 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);
|
409
src/fw/services/normal/blob_db/ios_notif_pref_db.c
Normal file
409
src/fw/services/normal/blob_db/ios_notif_pref_db.c
Normal 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(¬if_prefs->attr_list, ¬if_prefs->action_group,
|
||||
&buffer, serialized_prefs->num_attributes, serialized_prefs->num_actions,
|
||||
attributes_per_action);
|
||||
|
||||
if (!attributes_actions_deserialize(¬if_prefs->attr_list,
|
||||
¬if_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);
|
||||
}
|
81
src/fw/services/normal/blob_db/ios_notif_pref_db.h
Normal file
81
src/fw/services/normal/blob_db/ios_notif_pref_db.h
Normal 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
|
93
src/fw/services/normal/blob_db/notif_db.c
Normal file
93
src/fw/services/normal/blob_db/notif_db.c
Normal 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(¬ification, 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(¬ification.header.id)) {
|
||||
notification_storage_set_status(¬ification.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(¬ification);
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Notification added: %s", uuid_string);
|
||||
notifications_handle_notification_added(id);
|
||||
}
|
||||
|
||||
timeline_item_free_allocated_buffer(¬ification);
|
||||
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;
|
||||
}
|
36
src/fw/services/normal/blob_db/notif_db.h
Normal file
36
src/fw/services/normal/blob_db/notif_db.h
Normal 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);
|
276
src/fw/services/normal/blob_db/pin_db.c
Normal file
276
src/fw/services/normal/blob_db/pin_db.c
Normal 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);
|
||||
}
|
78
src/fw/services/normal/blob_db/pin_db.h
Normal file
78
src/fw/services/normal/blob_db/pin_db.h
Normal 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);
|
60
src/fw/services/normal/blob_db/prefs_db.c
Normal file
60
src/fw/services/normal/blob_db/prefs_db.c
Normal 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;
|
||||
}
|
35
src/fw/services/normal/blob_db/prefs_db.h
Normal file
35
src/fw/services/normal/blob_db/prefs_db.h
Normal 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);
|
284
src/fw/services/normal/blob_db/reminder_db.c
Normal file
284
src/fw/services/normal/blob_db/reminder_db.c
Normal 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);
|
||||
}
|
93
src/fw/services/normal/blob_db/reminder_db.h
Normal file
93
src/fw/services/normal/blob_db/reminder_db.h
Normal 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);
|
241
src/fw/services/normal/blob_db/sync.c
Normal file
241
src/fw/services/normal/blob_db/sync.c
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
66
src/fw/services/normal/blob_db/sync.h
Normal file
66
src/fw/services/normal/blob_db/sync.h
Normal 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);
|
56
src/fw/services/normal/blob_db/sync_util.c
Normal file
56
src/fw/services/normal/blob_db/sync_util.c
Normal 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;
|
||||
}
|
32
src/fw/services/normal/blob_db/sync_util.h
Normal file
32
src/fw/services/normal/blob_db/sync_util.h
Normal 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);
|
435
src/fw/services/normal/blob_db/timeline_item_storage.c
Normal file
435
src/fw/services/normal/blob_db/timeline_item_storage.c
Normal 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;
|
||||
}
|
89
src/fw/services/normal/blob_db/timeline_item_storage.h
Normal file
89
src/fw/services/normal/blob_db/timeline_item_storage.h
Normal 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);
|
31
src/fw/services/normal/blob_db/util.c
Normal file
31
src/fw/services/normal/blob_db/util.c
Normal 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);
|
||||
}
|
||||
}
|
21
src/fw/services/normal/blob_db/util.h
Normal file
21
src/fw/services/normal/blob_db/util.h
Normal 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);
|
230
src/fw/services/normal/blob_db/watch_app_prefs_db.c
Normal file
230
src/fw/services/normal/blob_db/watch_app_prefs_db.c
Normal 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;
|
||||
}
|
58
src/fw/services/normal/blob_db/watch_app_prefs_db.h
Normal file
58
src/fw/services/normal/blob_db/watch_app_prefs_db.h
Normal 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);
|
290
src/fw/services/normal/blob_db/weather_db.c
Normal file
290
src/fw/services/normal/blob_db/weather_db.c
Normal 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
|
79
src/fw/services/normal/blob_db/weather_db.h
Normal file
79
src/fw/services/normal/blob_db/weather_db.h
Normal 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);
|
Loading…
Add table
Add a link
Reference in a new issue