diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e4a354df64..1827892949 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -461,6 +461,7 @@ list(APPEND SOURCE_FILES components/ble/SimpleWeatherService.cpp components/ble/NavigationService.cpp components/ble/BatteryInformationService.cpp + components/ble/AppleNotificationCenterClient.cpp components/ble/FSService.cpp components/ble/ImmediateAlertService.cpp components/ble/ServiceDiscovery.cpp @@ -531,6 +532,7 @@ list(APPEND RECOVERY_SOURCE_FILES components/ble/MusicService.cpp components/ble/SimpleWeatherService.cpp components/ble/BatteryInformationService.cpp + components/ble/AppleNotificationCenterClient.cpp components/ble/FSService.cpp components/ble/ImmediateAlertService.cpp components/ble/ServiceDiscovery.cpp @@ -648,6 +650,7 @@ set(INCLUDE_FILES components/ble/DeviceInformationService.h components/ble/CurrentTimeClient.h components/ble/AlertNotificationClient.h + components/ble/AppleNotificationCenterClient.h components/ble/DfuService.h components/firmwarevalidator/FirmwareValidator.h components/ble/BatteryInformationService.h diff --git a/src/components/ble/AppleNotificationCenterClient.cpp b/src/components/ble/AppleNotificationCenterClient.cpp new file mode 100644 index 0000000000..f7e200e9ce --- /dev/null +++ b/src/components/ble/AppleNotificationCenterClient.cpp @@ -0,0 +1,562 @@ +#include "components/ble/AppleNotificationCenterClient.h" +#include "components/ble/NotificationManager.h" +#include "systemtask/SystemTask.h" +#include + +using namespace Pinetime::Controllers; + +int OnDiscoveryEventCallback(uint16_t conn_handle, const struct ble_gatt_error* error, const struct ble_gatt_svc* service, void* arg) { + auto client = static_cast(arg); + return client->OnDiscoveryEvent(conn_handle, error, service); +} + +int OnANCSCharacteristicDiscoveredCallback(uint16_t conn_handle, + const struct ble_gatt_error* error, + const struct ble_gatt_chr* chr, + void* arg) { + auto client = static_cast(arg); + return client->OnCharacteristicsDiscoveryEvent(conn_handle, error, chr); +} + +int OnANCSDescriptorDiscoveryEventCallback(uint16_t conn_handle, + const struct ble_gatt_error* error, + uint16_t chr_val_handle, + const struct ble_gatt_dsc* dsc, + void* arg) { + auto client = static_cast(arg); + return client->OnDescriptorDiscoveryEventCallback(conn_handle, error, chr_val_handle, dsc); +} + +int NewAlertSubcribeCallback(uint16_t conn_handle, const struct ble_gatt_error* error, struct ble_gatt_attr* attr, void* arg) { + auto client = static_cast(arg); + return client->OnNewAlertSubcribe(conn_handle, error, attr); +} + +int OnControlPointWriteCallback(uint16_t conn_handle, const struct ble_gatt_error* error, struct ble_gatt_attr* attr, void* arg) { + auto client = static_cast(arg); + return client->OnControlPointWrite(conn_handle, error, attr); +} + +AppleNotificationCenterClient::AppleNotificationCenterClient(Pinetime::System::SystemTask& systemTask, + Pinetime::Controllers::NotificationManager& notificationManager) + : systemTask {systemTask}, notificationManager {notificationManager} { +} + +bool AppleNotificationCenterClient::OnDiscoveryEvent(uint16_t connectionHandle, const ble_gatt_error* error, const ble_gatt_svc* service) { + if (service == nullptr && error->status == BLE_HS_EDONE) { + if (isDiscovered) { + NRF_LOG_INFO("ANCS Discovery found, starting characteristics discovery"); + ble_gattc_disc_all_chrs(connectionHandle, ancsStartHandle, ancsEndHandle, OnANCSCharacteristicDiscoveredCallback, this); + } else { + NRF_LOG_INFO("ANCS not found"); + MaybeFinishDiscovery(connectionHandle); + } + return true; + } + + if (service != nullptr && ble_uuid_cmp(&ancsUuid.u, &service->uuid.u) == 0) { + NRF_LOG_INFO("ANCS discovered : 0x%x - 0x%x", service->start_handle, service->end_handle); + + ancsStartHandle = service->start_handle; + ancsEndHandle = service->end_handle; + isDiscovered = true; + } + return false; +} + +int AppleNotificationCenterClient::OnCharacteristicsDiscoveryEvent(uint16_t connectionHandle, + const ble_gatt_error* error, + const ble_gatt_chr* characteristic) { + if (error->status != 0 && error->status != BLE_HS_EDONE) { + NRF_LOG_INFO("ANCS Characteristic discovery ERROR"); + MaybeFinishDiscovery(connectionHandle); + return 0; + } + + if (characteristic == nullptr && error->status == BLE_HS_EDONE) { + NRF_LOG_INFO("ANCS Characteristic discovery complete"); + if (isCharacteristicDiscovered) { + ble_gattc_disc_all_dscs(connectionHandle, notificationSourceHandle, ancsEndHandle, OnANCSDescriptorDiscoveryEventCallback, this); + } + if (isDataCharacteristicDiscovered) { + ble_gattc_disc_all_dscs(connectionHandle, dataSourceHandle, ancsEndHandle, OnANCSDescriptorDiscoveryEventCallback, this); + } + if (isCharacteristicDiscovered == isControlCharacteristicDiscovered && isCharacteristicDiscovered == isDataCharacteristicDiscovered) { + MaybeFinishDiscovery(connectionHandle); + } + } else if (characteristic != nullptr) { + if (ble_uuid_cmp(¬ificationSourceChar.u, &characteristic->uuid.u) == 0) { + NRF_LOG_INFO("ANCS Characteristic discovered: Notification Source"); + notificationSourceHandle = characteristic->val_handle; + isCharacteristicDiscovered = true; + } else if (ble_uuid_cmp(&controlPointChar.u, &characteristic->uuid.u) == 0) { + NRF_LOG_INFO("ANCS Characteristic discovered: Control Point"); + controlPointHandle = characteristic->val_handle; + isControlCharacteristicDiscovered = true; + } else if (ble_uuid_cmp(&dataSourceChar.u, &characteristic->uuid.u) == 0) { + NRF_LOG_INFO("ANCS Characteristic discovered: Data Source"); + dataSourceHandle = characteristic->val_handle; + isDataCharacteristicDiscovered = true; + } + } + return 0; +} + +int AppleNotificationCenterClient::OnDescriptorDiscoveryEventCallback(uint16_t connectionHandle, + const ble_gatt_error* error, + uint16_t characteristicValueHandle, + const ble_gatt_dsc* descriptor) { + if (error->status == 0) { + if (characteristicValueHandle == notificationSourceHandle && ble_uuid_cmp(¬ificationSourceChar.u, &descriptor->uuid.u)) { + if (notificationSourceDescriptorHandle == 0) { + NRF_LOG_INFO("ANCS Descriptor discovered : %d", descriptor->handle); + notificationSourceDescriptorHandle = descriptor->handle; + isDescriptorFound = true; + uint8_t value[2] {1, 0}; + ble_gattc_write_flat(connectionHandle, notificationSourceDescriptorHandle, value, sizeof(value), NewAlertSubcribeCallback, this); + ble_gattc_write_flat(connectionHandle, ancsEndHandle, value, sizeof(value), NewAlertSubcribeCallback, this); + } + } else if (characteristicValueHandle == controlPointHandle && ble_uuid_cmp(&controlPointChar.u, &descriptor->uuid.u)) { + if (controlPointDescriptorHandle == 0) { + NRF_LOG_INFO("ANCS Descriptor discovered : %d", descriptor->handle); + controlPointDescriptorHandle = descriptor->handle; + isControlDescriptorFound = true; + } + } else if (characteristicValueHandle == dataSourceHandle && ble_uuid_cmp(&dataSourceChar.u, &descriptor->uuid.u)) { + if (dataSourceDescriptorHandle == 0) { + NRF_LOG_INFO("ANCS Descriptor discovered : %d", descriptor->handle); + dataSourceDescriptorHandle = descriptor->handle; + isDataDescriptorFound = true; + uint8_t value[2] {1, 0}; + ble_gattc_write_flat(connectionHandle, dataSourceDescriptorHandle, value, sizeof(value), NewAlertSubcribeCallback, this); + } + } + } else { + if (error->status != BLE_HS_EDONE) { + char errorStr[55]; + snprintf(errorStr, sizeof(errorStr), "ANCS Descriptor discovery ERROR: %d", error->status); + NRF_LOG_INFO(errorStr); + } + if (isDescriptorFound == isDataDescriptorFound) + MaybeFinishDiscovery(connectionHandle); + } + return 0; +} + +int AppleNotificationCenterClient::OnNewAlertSubcribe(uint16_t connectionHandle, + const ble_gatt_error* error, + ble_gatt_attr* /*attribute*/) { + if (error->status == 0) { + NRF_LOG_INFO("ANCS New alert subscribe OK"); + + // Mark subscriptions complete only after both CCCDs are known + if (notificationSourceDescriptorHandle != 0 && dataSourceDescriptorHandle != 0) { + subscriptionsDone = true; + } + } else { + NRF_LOG_INFO("ANCS New alert subscribe ERROR"); + } + if (isDescriptorFound == isControlDescriptorFound && isDescriptorFound == isDataDescriptorFound) + MaybeFinishDiscovery(connectionHandle); + + return 0; +} + +int AppleNotificationCenterClient::OnControlPointWrite(uint16_t /*connectionHandle*/, + const ble_gatt_error* error, + ble_gatt_attr* /*attribute*/) { + if (error->status == 0) { + NRF_LOG_INFO("ANCS Control Point write OK"); + } else { + char errorStr[55]; + snprintf(errorStr, sizeof(errorStr), "ANCS Control Point ERROR: %d", error->status); + NRF_LOG_INFO(errorStr); + } + return 0; +} + +void AppleNotificationCenterClient::MaybeFinishDiscovery(uint16_t connectionHandle) { + if (isCharacteristicDiscovered && isControlCharacteristicDiscovered && isDataCharacteristicDiscovered && isDescriptorFound && + isControlDescriptorFound && isDataDescriptorFound && subscriptionsDone) { + onServiceDiscovered(connectionHandle); + } +} + +void AppleNotificationCenterClient::OnNotification(ble_gap_event* event) { + if (event->notify_rx.attr_handle == notificationSourceHandle || event->notify_rx.attr_handle == notificationSourceDescriptorHandle) { + NRF_LOG_INFO("ANCS Notification received"); + + AncsNotification ancsNotif; + + os_mbuf_copydata(event->notify_rx.om, 0, 1, &ancsNotif.eventId); + os_mbuf_copydata(event->notify_rx.om, 1, 1, &ancsNotif.eventFlags); + os_mbuf_copydata(event->notify_rx.om, 2, 1, &ancsNotif.category); + // Can be used to see how many grouped notifications are present + // os_mbuf_copydata(event->notify_rx.om, 3, 1, &categoryCount); + os_mbuf_copydata(event->notify_rx.om, 4, 4, &ancsNotif.uuid); + + // bool silent = (ancsNotif.eventFlags & static_cast(EventFlags::Silent)) != 0; + // bool important = eventFlags & static_cast(EventFlags::Important); + // bool preExisting = (ancsNotif.eventFlags & static_cast(EventFlags::PreExisting)) != 0; + // bool positiveAction = eventFlags & static_cast(EventFlags::PositiveAction); + // bool negativeAction = eventFlags & static_cast(EventFlags::NegativeAction); + + // If notification was removed, we remove it from the notifications map + if (ancsNotif.eventId == static_cast(EventIds::Removed) && notifications.contains(ancsNotif.uuid)) { + notifications.erase(ancsNotif.uuid); + NRF_LOG_INFO("ANCS Notification removed: %d", ancsNotif.uuid); + return; + } + + // If the notification is pre-existing, or if it is a silent notification, we do not add it to the list + if (notifications.contains(ancsNotif.uuid) || (ancsNotif.eventFlags & static_cast(EventFlags::Silent)) != 0 || + (ancsNotif.eventFlags & static_cast(EventFlags::PreExisting)) != 0) { + return; + } + + // If new notification, add it to the notifications + if (ancsNotif.eventId == static_cast(EventIds::Added) && + (ancsNotif.eventFlags & static_cast(EventFlags::Silent)) == 0) { + notifications.insert({ancsNotif.uuid, ancsNotif}); + } else { + // If the notification is not added, we ignore it + NRF_LOG_INFO("ANCS Notification not added, ignoring: %d", ancsNotif.uuid); + return; + } + + // The 6 is from TotalNbNotifications in NotificationManager.h + 1 + while (notifications.size() > 100) { + notifications.erase(notifications.begin()); + } + + // if (notifications.contains(ancsNotif.uuid)) { + // notifications[ancsNotif.uuid] = ancsNotif; + // } else { + // notifications.insert({ancsNotif.uuid, ancsNotif}); + // } + + // Request ANCS more info + // The +4 is for the "..." at the end of the string + uint8_t titleSize = maxTitleSize + 4; + uint8_t subTitleSize = maxSubtitleSize + 4; + uint8_t messageSize = maxMessageSize + 4; + BYTE request[14]; + request[0] = 0x00; // Command ID: Get Notification Attributes + request[1] = static_cast(ancsNotif.uuid & 0xFF); + request[2] = static_cast((ancsNotif.uuid >> 8) & 0xFF); + request[3] = static_cast((ancsNotif.uuid >> 16) & 0xFF); + request[4] = static_cast((ancsNotif.uuid >> 24) & 0xFF); + request[5] = 0x01; // Attribute ID: Title + // request[6] = 0x00; + request[6] = static_cast(titleSize & 0xFF); + request[7] = static_cast((titleSize >> 8) & 0xFF); + request[8] = 0x02; // Attribute ID: Subtitle + request[9] = static_cast(subTitleSize & 0xFF); + request[10] = static_cast((subTitleSize >> 8) & 0xFF); + request[11] = 0x03; // Attribute ID: Message + request[12] = static_cast(messageSize & 0xFF); + request[13] = static_cast((messageSize >> 8) & 0xFF); + + ble_gattc_write_flat(event->notify_rx.conn_handle, controlPointHandle, request, sizeof(request), OnControlPointWriteCallback, this); + } else if (event->notify_rx.attr_handle == dataSourceHandle || event->notify_rx.attr_handle == dataSourceDescriptorHandle) { + uint16_t titleSize; + uint16_t subTitleSize; + uint16_t messageSize; + uint32_t notificationUid; + + os_mbuf_copydata(event->notify_rx.om, 1, 4, ¬ificationUid); + os_mbuf_copydata(event->notify_rx.om, 6, 2, &titleSize); + os_mbuf_copydata(event->notify_rx.om, 8 + titleSize + 1, 2, &subTitleSize); + os_mbuf_copydata(event->notify_rx.om, 8 + titleSize + 1 + 2 + subTitleSize + 1, 2, &messageSize); + + AncsNotification ancsNotif; + ancsNotif.uuid = 0; + + // Check if the notification is in the session + if (notifications.contains(notificationUid)) { + if (notifications[notificationUid].isProcessed) { + // If the notification is already processed, we ignore it + NRF_LOG_INFO("Notification with UID %d already processed, ignoring", notificationUid); + return; + } + } else { + // If the notification is not in the session, we ignore it + NRF_LOG_INFO("Notification with UID %d not found in session, ignoring", notificationUid); + return; + } + + if (notifications.contains(notificationUid)) { + ancsNotif = notifications[notificationUid]; + } else { + // If the Notification source didn't add it earlier, then don't process it + NRF_LOG_INFO("Notification with UID %d not found in notifications map, ignoring datasource", notificationUid); + return; + } + + std::string decodedTitle = DecodeUtf8String(event->notify_rx.om, titleSize, 8); + + std::string decodedSubTitle = DecodeUtf8String(event->notify_rx.om, subTitleSize, 8 + titleSize + 1 + 2); + + std::string decodedMessage = DecodeUtf8String(event->notify_rx.om, messageSize, 8 + titleSize + 1 + 2 + subTitleSize + 1 + 2); + + // Debug event ids ands flags by putting them at front of message (in int format) + // decodedMessage = std::to_string(ancsNotif.uuid) + " " + decodedMessage; + + NRF_LOG_INFO("Decoded Title: %s", decodedTitle.c_str()); + NRF_LOG_INFO("Decoded SubTitle: %s", decodedSubTitle.c_str()); + + bool incomingCall = ancsNotif.uuid != 0 && ancsNotif.category == static_cast(Categories::IncomingCall); + + if (!incomingCall) { + if (titleSize >= maxTitleSize) { + decodedTitle.resize(maxTitleSize - 3); + decodedTitle += "..."; + if (!decodedSubTitle.empty()) { + decodedTitle += " - "; + } else { + decodedTitle += "-"; + } + } else { + if (!decodedSubTitle.empty()) { + decodedTitle += " - "; + } else { + decodedTitle += "-"; + } + } + + if (subTitleSize > maxSubtitleSize) { + decodedSubTitle.resize(maxSubtitleSize - 3); + decodedSubTitle += "..."; + } + } + + titleSize = static_cast(decodedTitle.size()); + subTitleSize = static_cast(decodedSubTitle.size()); + messageSize = static_cast(decodedMessage.size()); + + NotificationManager::Notification notif; + notif.ancsUid = notificationUid; + std::string notifStr; + + if (incomingCall) { + notifStr += "Incoming Call:"; + notifStr += decodedTitle; + notifStr += "\n"; + notifStr += decodedSubTitle; + } else { + notifStr += decodedTitle; + if (!decodedSubTitle.empty()) { + notifStr += decodedSubTitle + ":"; + } + notifStr += decodedMessage; + } + + // Adjust notification if too long + if (notifStr.size() > NotificationManager::MessageSize) { + notifStr.resize(97); + notifStr += "..."; + } + + notif.message = std::array {}; + std::strncpy(notif.message.data(), notifStr.c_str(), notif.message.size() - 1); + + if (incomingCall) { + notif.message[13] = '\0'; // Separate Title and Message + } else if (!decodedSubTitle.empty()) { + notif.message[titleSize + subTitleSize] = '\0'; // Separate Title and Message + } else { + notif.message[titleSize - 1] = '\0'; // Separate Title and Message + } + + notif.message[notif.message.size() - 1] = '\0'; // Ensure null-termination + notif.size = std::min(std::strlen(notifStr.c_str()), notif.message.size()); + if (incomingCall) { + notif.category = Pinetime::Controllers::NotificationManager::Categories::IncomingCall; + } else { + notif.category = Pinetime::Controllers::NotificationManager::Categories::SimpleAlert; + } + notificationManager.Push(std::move(notif)); + + // Only ping the system task if the notification was added and ignore pre-existing notifications + if (ancsNotif.isProcessed == false && (ancsNotif.eventFlags & static_cast(EventFlags::Silent)) == 0 && + (ancsNotif.eventFlags & static_cast(EventFlags::PreExisting)) == 0) { + systemTask.PushMessage(Pinetime::System::Messages::OnNewNotification); + } + + // Mark the notification as processed in the session + notifications[notificationUid].isProcessed = true; + } +} + +void AppleNotificationCenterClient::AcceptIncomingCall(uint32_t uuid) { + if (notifications.contains(uuid)) { + const AncsNotification ancsNotif = notifications[uuid]; + if (ancsNotif.category != static_cast(Categories::IncomingCall)) { + return; + } + } else { + return; + } + + uint8_t value[6]; + value[0] = 0x02; // Command ID: Perform Notification Action + value[1] = static_cast((uuid & 0xFF)); + value[2] = static_cast((uuid >> 8) & 0xFF); + value[3] = static_cast((uuid >> 16) & 0xFF); + value[4] = static_cast((uuid >> 24) & 0xFF); + value[5] = 0x00; // Action ID: Positive Action + + ble_gattc_write_flat(systemTask.nimble().connHandle(), controlPointHandle, value, sizeof(value), OnControlPointWriteCallback, this); +} + +void AppleNotificationCenterClient::RejectIncomingCall(uint32_t uuid) { + AncsNotification ancsNotif; + if (notifications.contains(uuid)) { + ancsNotif = notifications[uuid]; + if (ancsNotif.category != static_cast(Categories::IncomingCall)) { + return; + } + } else { + return; + } + + uint8_t value[6]; + value[0] = 0x02; // Command ID: Perform Notification Action + value[1] = static_cast(uuid & 0xFF); + value[2] = static_cast((uuid >> 8) & 0xFF); + value[3] = static_cast((uuid >> 16) & 0xFF); + value[4] = static_cast((uuid >> 24) & 0xFF); + value[5] = 0x01; // Action ID: Negative Action + + ble_gattc_write_flat(systemTask.nimble().connHandle(), controlPointHandle, value, sizeof(value), OnControlPointWriteCallback, this); +} + +void AppleNotificationCenterClient::Reset() { + ancsStartHandle = 0; + ancsEndHandle = 0; + gattStartHandle = 0; + gattEndHandle = 0; + serviceChangedHandle = 0; + serviceChangedDescriptorHandle = 0; + notificationSourceHandle = 0; + notificationSourceDescriptorHandle = 0; + controlPointHandle = 0; + controlPointDescriptorHandle = 0; + dataSourceHandle = 0; + dataSourceDescriptorHandle = 0; + isGattDiscovered = false; + isGattCharacteristicDiscovered = false; + isGattDescriptorFound = false; + isDiscovered = false; + isCharacteristicDiscovered = false; + isDescriptorFound = false; + isControlCharacteristicDiscovered = false; + isControlDescriptorFound = false; + isDataCharacteristicDiscovered = false; + isDataDescriptorFound = false; + subscriptionsDone = false; + + notifications.clear(); +} + +void AppleNotificationCenterClient::Discover(uint16_t connectionHandle, std::function onServiceDiscovered) { + NRF_LOG_INFO("[ANCS] Starting discovery"); + this->onServiceDiscovered = onServiceDiscovered; + ble_gattc_disc_svc_by_uuid(connectionHandle, &ancsUuid.u, OnDiscoveryEventCallback, this); +} + +// This function is used for debugging purposes to log a message and push a notification +// Used to test BLE debugging on production devices +void AppleNotificationCenterClient::DebugNotification(const char* msg) const { + NRF_LOG_INFO("[ANCS DEBUG] %s", msg); + + NotificationManager::Notification notif; + std::strncpy(notif.message.data(), msg, notif.message.size() - 1); + notif.message[notif.message.size() - 1] = '\0'; // Ensure null-termination + notif.size = std::min(std::strlen(msg), notif.message.size()); + notif.category = Pinetime::Controllers::NotificationManager::Categories::SimpleAlert; + notificationManager.Push(std::move(notif)); + + systemTask.PushMessage(Pinetime::System::Messages::OnNewNotification); +} + +std::string AppleNotificationCenterClient::DecodeUtf8String(os_mbuf* om, uint16_t size, uint16_t offset) { + std::string decoded; + decoded.reserve(size); + + auto isInFontDefinition = [](uint32_t codepoint) -> bool { + // Check if the codepoint falls into the specified font ranges or is explicitly listed + return (codepoint >= 0x20 && codepoint <= 0x7E) || // Printable ASCII + (codepoint >= 0x410 && codepoint <= 0x44F) || // Cyrillic + codepoint == 0xB0; + }; + + for (uint16_t i = 0; i < size;) { + uint8_t byte = 0; + if (os_mbuf_copydata(om, offset + i, 1, &byte) != 0) { + break; // Handle error in copying data (e.g., log or terminate processing) + } + + if (byte <= 0x7F) { // Single-byte UTF-8 (ASCII) + if (isInFontDefinition(byte)) { + decoded.push_back(static_cast(byte)); + } else { + decoded.append("�"); // Replace unsupported + } + ++i; + } else { // Multi-byte UTF-8 + // Determine sequence length based on leading byte + int sequenceLength = 0; + if ((byte & 0xE0) == 0xC0) { + sequenceLength = 2; // 2-byte sequence + } else if ((byte & 0xF0) == 0xE0) { + sequenceLength = 3; // 3-byte sequence + } else if ((byte & 0xF8) == 0xF0) { + sequenceLength = 4; // 4-byte sequence + } + + if (i + sequenceLength > size) { + decoded.append("�"); // Incomplete sequence, replace + break; + } + + // Read the full sequence + std::string utf8Char; + bool validSequence = true; + uint32_t codepoint = 0; + + for (int j = 0; j < sequenceLength; ++j) { + uint8_t nextByte = 0; + os_mbuf_copydata(om, offset + i + j, 1, &nextByte); + utf8Char.push_back(static_cast(nextByte)); + + if (j == 0) { + // Leading byte contributes significant bits + if (sequenceLength == 2) { + codepoint = nextByte & 0x1F; + } else if (sequenceLength == 3) { + codepoint = nextByte & 0x0F; + } else if (sequenceLength == 4) { + codepoint = nextByte & 0x07; + } + } else { + // Continuation bytes contribute lower bits + if ((nextByte & 0xC0) != 0x80) { + validSequence = false; // Invalid UTF-8 continuation byte + break; + } + codepoint = (codepoint << 6) | (nextByte & 0x3F); + } + } + + if (validSequence && isInFontDefinition(codepoint)) { + decoded.append(utf8Char); // Append valid UTF-8 character + } else { + decoded.append("�"); // Replace unsupported + } + i += sequenceLength; + } + } + + return decoded; +} diff --git a/src/components/ble/AppleNotificationCenterClient.h b/src/components/ble/AppleNotificationCenterClient.h new file mode 100644 index 0000000000..43c0d20e18 --- /dev/null +++ b/src/components/ble/AppleNotificationCenterClient.h @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#define min // workaround: nimble's min/max macros conflict with libstdc++ +#define max +#include +#undef max +#undef min +#include "components/ble/BleClient.h" +#include +#include + +namespace Pinetime { + + namespace System { + class SystemTask; + } + + namespace Controllers { + class NotificationManager; + + class AppleNotificationCenterClient : public BleClient { + public: + explicit AppleNotificationCenterClient(Pinetime::System::SystemTask& systemTask, + Pinetime::Controllers::NotificationManager& notificationManager); + + bool OnDiscoveryEvent(uint16_t connectionHandle, const ble_gatt_error* error, const ble_gatt_svc* service); + int OnCharacteristicsDiscoveryEvent(uint16_t connectionHandle, const ble_gatt_error* error, const ble_gatt_chr* characteristic); + int OnNewAlertSubcribe(uint16_t connectionHandle, const ble_gatt_error* error, ble_gatt_attr* attribute); + int OnDescriptorDiscoveryEventCallback(uint16_t connectionHandle, + const ble_gatt_error* error, + uint16_t characteristicValueHandle, + const ble_gatt_dsc* descriptor); + int OnControlPointWrite(uint16_t connectionHandle, const ble_gatt_error* error, ble_gatt_attr* attribute); + void MaybeFinishDiscovery(uint16_t connectionHandle); + void OnNotification(ble_gap_event* event); + void Reset(); + void Discover(uint16_t connectionHandle, std::function lambda) override; + void DebugNotification(const char* msg) const; + + void AcceptIncomingCall(uint32_t notificationUid); + void RejectIncomingCall(uint32_t notificationUid); + + static constexpr uint8_t maxTitleSize {20}; + static constexpr uint8_t maxSubtitleSize {15}; + static constexpr uint8_t maxMessageSize {120}; + + // The Apple Notification Center Service UUID are from + // https://developer.apple.com/library/archive/documentation/CoreBluetooth/Reference/AppleNotificationCenterServiceSpecification/Specification/Specification.html + + // 7905F431-B5CE-4E99-A40F-4B1E122D00D0 + static constexpr ble_uuid128_t ancsUuid { + .u {.type = BLE_UUID_TYPE_128}, + .value = {0xd0, 0x00, 0x2D, 0x12, 0x1E, 0x4B, 0x0F, 0xA4, 0x99, 0x4E, 0xCE, 0xB5, 0x31, 0xF4, 0x05, 0x79}}; + + private: + // 9FBF120D-6301-42D9-8C58-25E699A21DBD + static constexpr ble_uuid128_t notificationSourceChar { + .u {.type = BLE_UUID_TYPE_128}, + .value = {0xBD, 0x1D, 0xA2, 0x99, 0xE6, 0x25, 0x58, 0x8C, 0xD9, 0x42, 0x01, 0x63, 0x0D, 0x12, 0xBF, 0x9F}}; + // 69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9 + static constexpr ble_uuid128_t controlPointChar { + .u {.type = BLE_UUID_TYPE_128}, + .value = {0xD9, 0xD9, 0xAA, 0xFD, 0xBD, 0x9B, 0x21, 0x98, 0xA8, 0x49, 0xE1, 0x45, 0xF3, 0xD8, 0xD1, 0x69}}; + // 22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB + static constexpr ble_uuid128_t dataSourceChar { + .u {.type = BLE_UUID_TYPE_128}, + .value = {0xFB, 0x7B, 0x7C, 0xCE, 0x6A, 0xB3, 0x44, 0xBE, 0xB5, 0x4B, 0xD6, 0x24, 0xE9, 0xC6, 0xEA, 0x22}}; + + static constexpr ble_uuid16_t gattServiceUuid = {BLE_UUID_TYPE_16, 0x1801}; + static constexpr ble_uuid16_t serviceChangedCharUuid = {BLE_UUID_TYPE_16, 0x2A05}; + + enum class Categories : uint8_t { + Other = 0, + IncomingCall = 1, + MissedCall = 2, + Voicemail = 3, + Social = 4, + Schedule = 5, + Email = 6, + News = 7, + HealthAndFitness = 8, + BuissnessAndFinance = 9, + Location = 10, + Entertainment = 11 + }; + + enum class EventIds : uint8_t { Added = 0, Modified = 1, Removed = 2 }; + + enum class EventFlags : uint8_t { + Silent = (1 << 0), + Important = (1 << 1), + PreExisting = (1 << 2), + PositiveAction = (1 << 3), + NegativeAction = (1 << 4) + }; + + struct AncsNotification { + uint8_t eventId {0}; + uint8_t eventFlags {0}; + uint8_t category {0}; + uint32_t uuid {0}; + bool isProcessed {false}; + }; + + std::unordered_map notifications; + + std::string DecodeUtf8String(os_mbuf* om, uint16_t size, uint16_t offset); + + bool subscriptionsDone = false; + uint16_t ancsStartHandle {0}; + uint16_t ancsEndHandle {0}; + uint16_t notificationSourceHandle {0}; + uint16_t controlPointHandle {0}; + uint16_t dataSourceHandle {0}; + uint16_t notificationSourceDescriptorHandle {0}; + uint16_t controlPointDescriptorHandle {0}; + uint16_t dataSourceDescriptorHandle {0}; + + uint16_t gattStartHandle {0}; + uint16_t gattEndHandle {0}; + uint16_t serviceChangedHandle {0}; + uint16_t serviceChangedDescriptorHandle {0}; + bool isGattDiscovered {false}; + bool isGattCharacteristicDiscovered {false}; + bool isGattDescriptorFound {false}; + bool isDiscovered {false}; + bool isCharacteristicDiscovered {false}; + bool isDescriptorFound {false}; + bool isControlCharacteristicDiscovered {false}; + bool isControlDescriptorFound {false}; + bool isDataCharacteristicDiscovered {false}; + bool isDataDescriptorFound {false}; + Pinetime::System::SystemTask& systemTask; + Pinetime::Controllers::NotificationManager& notificationManager; + std::function onServiceDiscovered; + }; + } +} diff --git a/src/components/ble/BatteryInformationService.cpp b/src/components/ble/BatteryInformationService.cpp index db7c856622..e776526661 100644 --- a/src/components/ble/BatteryInformationService.cpp +++ b/src/components/ble/BatteryInformationService.cpp @@ -17,7 +17,7 @@ BatteryInformationService::BatteryInformationService(Controllers::Battery& batte characteristicDefinition {{.uuid = &batteryLevelUuid.u, .access_cb = BatteryInformationServiceCallback, .arg = this, - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_READ_AUTHEN | BLE_GATT_CHR_F_NOTIFY, .val_handle = &batteryLevelHandle}, {0}}, serviceDefinition { diff --git a/src/components/ble/NimbleController.cpp b/src/components/ble/NimbleController.cpp index 5059007ab9..b40fe7a3c0 100644 --- a/src/components/ble/NimbleController.cpp +++ b/src/components/ble/NimbleController.cpp @@ -49,7 +49,8 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask, heartRateService {*this, heartRateController}, motionService {*this, motionController}, fsService {systemTask, fs}, - serviceDiscovery({¤tTimeClient, &alertNotificationClient}) { + ancsClient {systemTask, notificationManager}, + serviceDiscovery({¤tTimeClient, &alertNotificationClient, &ancsClient}) { } void nimble_on_reset(int reason) { @@ -161,8 +162,9 @@ void NimbleController::StartAdvertising() { fields.uuids16 = &HeartRateService::heartRateServiceUuid; fields.num_uuids16 = 1; fields.uuids16_is_complete = 1; - fields.uuids128 = &DfuService::serviceUuid; - fields.num_uuids128 = 1; + const ble_uuid128_t uuids128[2] = {DfuService::serviceUuid, AppleNotificationCenterClient::ancsUuid}; + fields.uuids128 = uuids128; + fields.num_uuids128 = 2; fields.uuids128_is_complete = 1; fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; @@ -200,6 +202,7 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { /* Connection failed; resume advertising. */ currentTimeClient.Reset(); alertNotificationClient.Reset(); + ancsClient.Reset(); connectionHandle = BLE_HS_CONN_HANDLE_NONE; bleController.Disconnect(); fastAdvCount = 0; @@ -209,6 +212,7 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { bleController.Connect(); systemTask.PushMessage(Pinetime::System::Messages::BleConnected); // Service discovery is deferred via systemtask + ble_gap_security_initiate(event->connect.conn_handle); } break; @@ -223,6 +227,7 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { currentTimeClient.Reset(); alertNotificationClient.Reset(); + ancsClient.Reset(); connectionHandle = BLE_HS_CONN_HANDLE_NONE; if (bleController.IsConnected()) { bleController.Disconnect(); @@ -370,6 +375,7 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { notifSize); alertNotificationClient.OnNotification(event); + ancsClient.OnNotification(event); } break; case BLE_GAP_EVENT_NOTIFY_TX: diff --git a/src/components/ble/NimbleController.h b/src/components/ble/NimbleController.h index 597ef0cc34..8f3a1dc3f7 100644 --- a/src/components/ble/NimbleController.h +++ b/src/components/ble/NimbleController.h @@ -23,6 +23,7 @@ #include "components/ble/MotionService.h" #include "components/ble/SimpleWeatherService.h" #include "components/fs/FS.h" +#include "components/ble/AppleNotificationCenterClient.h" namespace Pinetime { namespace Drivers { @@ -67,6 +68,10 @@ namespace Pinetime { return anService; }; + Pinetime::Controllers::AppleNotificationCenterClient& ancs() { + return ancsClient; + }; + Pinetime::Controllers::SimpleWeatherService& weather() { return weatherService; }; @@ -106,6 +111,7 @@ namespace Pinetime { HeartRateService heartRateService; MotionService motionService; FSService fsService; + AppleNotificationCenterClient ancsClient; ServiceDiscovery serviceDiscovery; uint8_t addrType; diff --git a/src/components/ble/NotificationManager.h b/src/components/ble/NotificationManager.h index 57a9c715d2..74eda10996 100644 --- a/src/components/ble/NotificationManager.h +++ b/src/components/ble/NotificationManager.h @@ -34,6 +34,8 @@ namespace Pinetime { Id id = 0; bool valid = false; + uint32_t ancsUid = 0; + const char* Message() const; const char* Title() const; }; diff --git a/src/components/ble/ServiceDiscovery.cpp b/src/components/ble/ServiceDiscovery.cpp index 03bcfeb47b..e467c014f2 100644 --- a/src/components/ble/ServiceDiscovery.cpp +++ b/src/components/ble/ServiceDiscovery.cpp @@ -4,7 +4,7 @@ using namespace Pinetime::Controllers; -ServiceDiscovery::ServiceDiscovery(std::array&& clients) : clients {clients} { +ServiceDiscovery::ServiceDiscovery(std::array&& clients) : clients {clients} { } void ServiceDiscovery::StartDiscovery(uint16_t connectionHandle) { diff --git a/src/components/ble/ServiceDiscovery.h b/src/components/ble/ServiceDiscovery.h index fc3b38c0a6..039862a442 100644 --- a/src/components/ble/ServiceDiscovery.h +++ b/src/components/ble/ServiceDiscovery.h @@ -9,13 +9,13 @@ namespace Pinetime { class ServiceDiscovery { public: - ServiceDiscovery(std::array&& bleClients); + ServiceDiscovery(std::array&& bleClients); void StartDiscovery(uint16_t connectionHandle); private: BleClient** clientIterator; - std::array clients; + std::array clients; void OnServiceDiscovered(uint16_t connectionHandle); void DiscoverNextService(uint16_t connectionHandle); }; diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index 35330fb7f7..7e875239c1 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -566,6 +566,7 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio currentScreen = std::make_unique(this, notificationManager, systemTask->nimble().alertService(), + systemTask->nimble().ancs(), motorController, *systemTask, Screens::Notifications::Modes::Normal); @@ -574,6 +575,7 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio currentScreen = std::make_unique(this, notificationManager, systemTask->nimble().alertService(), + systemTask->nimble().ancs(), motorController, *systemTask, Screens::Notifications::Modes::Preview); diff --git a/src/displayapp/fonts/fonts.json b/src/displayapp/fonts/fonts.json index 90be1febe6..5293ed7bb1 100644 --- a/src/displayapp/fonts/fonts.json +++ b/src/displayapp/fonts/fonts.json @@ -3,7 +3,7 @@ "sources": [ { "file": "JetBrainsMono-Bold.ttf", - "range": "0x20-0x7e, 0x410-0x44f, 0xB0" + "range": "0x20-0x7e, 0x410-0x44f, 0xB0, 0xFFFD" }, { "file": "FontAwesome5-Solid+Brands+Regular.woff", diff --git a/src/displayapp/screens/Notifications.cpp b/src/displayapp/screens/Notifications.cpp index 837c4683aa..54c761327b 100644 --- a/src/displayapp/screens/Notifications.cpp +++ b/src/displayapp/screens/Notifications.cpp @@ -2,6 +2,7 @@ #include "displayapp/DisplayApp.h" #include "components/ble/MusicService.h" #include "components/ble/AlertNotificationService.h" +#include "components/ble/AppleNotificationCenterClient.h" #include "displayapp/screens/Symbols.h" #include #include "displayapp/InfiniTimeTheme.h" @@ -13,12 +14,14 @@ extern lv_font_t jetbrains_mono_bold_20; Notifications::Notifications(DisplayApp* app, Pinetime::Controllers::NotificationManager& notificationManager, Pinetime::Controllers::AlertNotificationService& alertNotificationService, + Pinetime::Controllers::AppleNotificationCenterClient& ancsClient, Pinetime::Controllers::MotorController& motorController, System::SystemTask& systemTask, Modes mode) : app {app}, notificationManager {notificationManager}, alertNotificationService {alertNotificationService}, + ancsClient {ancsClient}, motorController {motorController}, wakeLock(systemTask), mode {mode} { @@ -33,10 +36,12 @@ Notifications::Notifications(DisplayApp* app, notification.category, notificationManager.NbNotifications(), alertNotificationService, - motorController); + ancsClient, + motorController, + notification.ancsUid); validDisplay = true; } else { - currentItem = std::make_unique(alertNotificationService, motorController); + currentItem = std::make_unique(alertNotificationService, ancsClient, motorController); validDisplay = false; } if (mode == Modes::Preview) { @@ -109,7 +114,9 @@ void Notifications::Refresh() { notification.category, notificationManager.NbNotifications(), alertNotificationService, - motorController); + ancsClient, + motorController, + notification.ancsUid); } else { running = false; } @@ -202,7 +209,9 @@ bool Notifications::OnTouchEvent(Pinetime::Applications::TouchEvents event) { previousNotification.category, notificationManager.NbNotifications(), alertNotificationService, - motorController); + ancsClient, + motorController, + previousNotification.ancsUid); } return true; case Pinetime::Applications::TouchEvents::SwipeUp: { @@ -229,7 +238,9 @@ bool Notifications::OnTouchEvent(Pinetime::Applications::TouchEvents event) { nextNotification.category, notificationManager.NbNotifications(), alertNotificationService, - motorController); + ancsClient, + motorController, + nextNotification.ancsUid); } return true; default: @@ -245,6 +256,7 @@ namespace { } Notifications::NotificationItem::NotificationItem(Pinetime::Controllers::AlertNotificationService& alertNotificationService, + Pinetime::Controllers::AppleNotificationCenterClient& ancsClient, Pinetime::Controllers::MotorController& motorController) : NotificationItem("Notifications", "No notifications to display", @@ -252,7 +264,9 @@ Notifications::NotificationItem::NotificationItem(Pinetime::Controllers::AlertNo Controllers::NotificationManager::Categories::Unknown, 0, alertNotificationService, - motorController) { + ancsClient, + motorController, + 0) { } Notifications::NotificationItem::NotificationItem(const char* title, @@ -261,8 +275,12 @@ Notifications::NotificationItem::NotificationItem(const char* title, Controllers::NotificationManager::Categories category, uint8_t notifNb, Pinetime::Controllers::AlertNotificationService& alertNotificationService, - Pinetime::Controllers::MotorController& motorController) - : alertNotificationService {alertNotificationService}, motorController {motorController} { + Pinetime::Controllers::AppleNotificationCenterClient& ancsClient, + Pinetime::Controllers::MotorController& motorController, + uint32_t ancsUid) + : alertNotificationService {alertNotificationService}, ancsClient {ancsClient}, motorController {motorController} { + this->ancsUid = ancsUid; + container = lv_cont_create(lv_scr_act(), nullptr); lv_obj_set_size(container, LV_HOR_RES, LV_VER_RES); lv_obj_set_style_local_bg_color(container, LV_CONT_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); @@ -360,8 +378,10 @@ void Notifications::NotificationItem::OnCallButtonEvent(lv_obj_t* obj, lv_event_ if (obj == bt_accept) { alertNotificationService.AcceptIncomingCall(); + ancsClient.AcceptIncomingCall(ancsUid); } else if (obj == bt_reject) { alertNotificationService.RejectIncomingCall(); + ancsClient.RejectIncomingCall(ancsUid); } else if (obj == bt_mute) { alertNotificationService.MuteIncomingCall(); } diff --git a/src/displayapp/screens/Notifications.h b/src/displayapp/screens/Notifications.h index 8488dc5bb2..18ab8e5a34 100644 --- a/src/displayapp/screens/Notifications.h +++ b/src/displayapp/screens/Notifications.h @@ -24,6 +24,7 @@ namespace Pinetime { explicit Notifications(DisplayApp* app, Pinetime::Controllers::NotificationManager& notificationManager, Pinetime::Controllers::AlertNotificationService& alertNotificationService, + Pinetime::Controllers::AppleNotificationCenterClient& ancsClient, Pinetime::Controllers::MotorController& motorController, System::SystemTask& systemTask, Modes mode); @@ -38,6 +39,7 @@ namespace Pinetime { class NotificationItem { public: NotificationItem(Pinetime::Controllers::AlertNotificationService& alertNotificationService, + Pinetime::Controllers::AppleNotificationCenterClient& ancsClient, Pinetime::Controllers::MotorController& motorController); NotificationItem(const char* title, const char* msg, @@ -45,13 +47,17 @@ namespace Pinetime { Controllers::NotificationManager::Categories, uint8_t notifNb, Pinetime::Controllers::AlertNotificationService& alertNotificationService, - Pinetime::Controllers::MotorController& motorController); + Pinetime::Controllers::AppleNotificationCenterClient& ancsClient, + Pinetime::Controllers::MotorController& motorController, + uint32_t ancsUid); ~NotificationItem(); bool IsRunning() const { return running; } + uint32_t ancsUid = 0; + void OnCallButtonEvent(lv_obj_t*, lv_event_t event); private: @@ -64,6 +70,7 @@ namespace Pinetime { lv_obj_t* label_mute; lv_obj_t* label_reject; Pinetime::Controllers::AlertNotificationService& alertNotificationService; + Pinetime::Controllers::AppleNotificationCenterClient& ancsClient; Pinetime::Controllers::MotorController& motorController; bool running = true; @@ -73,6 +80,7 @@ namespace Pinetime { DisplayApp* app; Pinetime::Controllers::NotificationManager& notificationManager; Pinetime::Controllers::AlertNotificationService& alertNotificationService; + Pinetime::Controllers::AppleNotificationCenterClient& ancsClient; Pinetime::Controllers::MotorController& motorController; System::WakeLock wakeLock; Modes mode = Modes::Normal;