Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson
2025-01-27 11:38:16 -08:00
committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions
+681
View File
@@ -0,0 +1,681 @@
/*
* 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 <stdint.h>
#include "resource/timeline_resource_ids.auto.h"
#include "util/attributes.h"
// Chunked dictionary 1/2 sent by ANCS (w/header)
// 00 97 00 00 00 00 13 00 63 6f 6d 2e 61 70 70 6c ........ com.appl
// 65 2e 4d 6f 62 69 6c 65 53 4d 53 01 10 00 72 6a e.Mobile SMS...rj
// 63 61 73 65 40 67 6d 61 69 6c 2e 63 6f 6d 02 00 case@gma il.com..
// 00 03 c8 00 54 68 69 73 20 69 73 20 61 20 76 65 ....This is a ve
// 72 79 20 63 6f 6d 70 6c 69 63 61 74 65 64 20 63 ry compl icated c
// 61 73 65 2c 20 4d 61 75 64 65 2e 20 59 6f 75 20 ase, Mau de. You
// 6b 6e 6f 77 2c 20 61 20 6c 6f 74 74 61 20 69 6e know, a lotta in
// 73 2c 20 6c 6f 74 74 61 20 6f 75 74 73 2c 20 6c s, lotta outs, l
// 6f 74 74 61 20 77 68 61 74 2d 68 61 76 65 2d 79 otta wha t-have-y
// 6f 75 27 73 2e 20 41 6e 64 2c 20 ou's. An d,
// NB header has been removed
static const uint8_t s_chunked_dict_part_one[] = {
0x00, 0x97, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c,
0x65, 0x2e, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x10, 0x00, 0x72, 0x6a,
0x63, 0x61, 0x73, 0x65, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x02, 0x00,
0x00, 0x03, 0xc8, 0x00, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x76, 0x65,
0x72, 0x79, 0x20, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x20, 0x63,
0x61, 0x73, 0x65, 0x2c, 0x20, 0x4d, 0x61, 0x75, 0x64, 0x65, 0x2e, 0x20, 0x59, 0x6f, 0x75, 0x20,
0x6b, 0x6e, 0x6f, 0x77, 0x2c, 0x20, 0x61, 0x20, 0x6c, 0x6f, 0x74, 0x74, 0x61, 0x20, 0x69, 0x6e,
0x73, 0x2c, 0x20, 0x6c, 0x6f, 0x74, 0x74, 0x61, 0x20, 0x6f, 0x75, 0x74, 0x73, 0x2c, 0x20, 0x6c,
0x6f, 0x74, 0x74, 0x61, 0x20, 0x77, 0x68, 0x61, 0x74, 0x2d, 0x68, 0x61, 0x76, 0x65, 0x2d, 0x79,
0x6f, 0x75, 0x27, 0x73, 0x2e, 0x20, 0x41, 0x6e, 0x64, 0x2c, 0x20
};
// Chunked dictionary 2/2 sent by ANCS
// 75 68 2c 20 6c 6f 74 74 61 20 73 74 72 61 6e 64 uh, lott a strand
// 73 20 74 6f 20 6b 65 65 70 20 69 6e 20 6d 79 20 s to kee p in my
// 68 65 61 64 2c 20 6d 61 6e 2e 20 4c 6f 74 74 61 head, ma n. Lotta
// 20 73 74 72 61 6e 64 73 20 69 6e 20 6f 6c 64 20 strands in old
// 44 75 64 65 72 27 73 20 68 65 61 64 2e 20 4c 75 Duder's head. Lu
// 63 6b 69 6c 79 20 49 27 6d 20 61 64 68 65 72 69 ckily I' m adheri
// 6e n
static const uint8_t s_chunked_dict_part_two[] = {
0x75, 0x68, 0x2c, 0x20, 0x6c, 0x6f, 0x74, 0x74, 0x61, 0x20, 0x73, 0x74, 0x72, 0x61, 0x6e, 0x64,
0x73, 0x20, 0x74, 0x6f, 0x20, 0x6b, 0x65, 0x65, 0x70, 0x20, 0x69, 0x6e, 0x20, 0x6d, 0x79, 0x20,
0x68, 0x65, 0x61, 0x64, 0x2c, 0x20, 0x6d, 0x61, 0x6e, 0x2e, 0x20, 0x4c, 0x6f, 0x74, 0x74, 0x61,
0x20, 0x73, 0x74, 0x72, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x6f, 0x6c, 0x64, 0x20,
0x44, 0x75, 0x64, 0x65, 0x72, 0x27, 0x73, 0x20, 0x68, 0x65, 0x61, 0x64, 0x2e, 0x20, 0x4c, 0x75,
0x63, 0x6b, 0x69, 0x6c, 0x79, 0x20, 0x49, 0x27, 0x6d, 0x20, 0x61, 0x64, 0x68, 0x65, 0x72, 0x69,
0x6e, /* date */ 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31,
0x33, 0x31, 0x31, 0x30, 0x35, 0x54, 0x31, 0x38, 0x31, 0x39, 0x32, 0x38
};
// Complete dictionary sent by ANCS
// 00 98 00 00 00 00 13 00 63 6f 6d 2e 61 70 70 6c ........ com.appl
// 65 2e 4d 6f 62 69 6c 65 53 4d 53 01 10 00 72 6a e.Mobile SMS...rj
// 63 61 73 65 40 67 6d 61 69 6c 2e 63 6f 6d 02 00 case@gma il.com..
// 00 03 26 00 43 61 72 65 66 75 6c 2c 20 6d 61 6e ..&.Care ful, man
// 2c 20 74 68 65 72 65 27 73 20 61 20 62 65 76 65 , there' s a beve
// 72 61 67 65 20 68 65 72 65 21 rage her e! // FIXME 4th
// attr has been appended
// Has header removed:
static const uint8_t s_complete_dict[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c,
0x65, 0x2e, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x10, 0x00, 0x72, 0x6a,
0x63, 0x61, 0x73, 0x65, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x02, 0x00,
0x00, 0x03, 0x26, 0x00, 0x43, 0x61, 0x72, 0x65, 0x66, 0x75, 0x6c, 0x2c, 0x20, 0x6d, 0x61, 0x6e,
0x2c, 0x20, 0x74, 0x68, 0x65, 0x72, 0x65, 0x27, 0x73, 0x20, 0x61, 0x20, 0x62, 0x65, 0x76, 0x65,
0x72, 0x61, 0x67, 0x65, 0x20, 0x68, 0x65, 0x72, 0x65, 0x21, /* date */ 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31,
0x33, 0x31, 0x31, 0x30, 0x35, 0x54, 0x31, 0x38, 0x31, 0x39, 0x32, 0x38
};
//! 00 3c 00 00 00 05 0f 00 32 30 31 36 30 33 30 31 ........ 20160301
//! 54 31 34 31 35 32 33 01 08 00 4f 76 65 72 63 61 T141523. ..Overca
//! 73 74 02 00 00 03 2b 00 54 68 65 20 4d 6f 74 68 st....+. The Moth
//! 20 50 6f 64 63 61 73 74 3a 20 4d 6f 6e 69 63 61 Podcast : Monica
//! 20 43 68 65 72 75 2d 4d 70 61 6d 62 61 77 61 73 Cheru-M pambawas
//! e2 80 a6 04 03 00 31 31 33 ......11 3
static const uint8_t s_message_size_attr_dict[] = {
0x00, 0x3c, 0x00, 0x00, 0x00, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x36, 0x30, 0x33, 0x30, 0x31,
0x54, 0x31, 0x34, 0x31, 0x35, 0x32, 0x33, 0x01, 0x08, 0x00, 0x4f, 0x76, 0x65, 0x72, 0x63, 0x61,
0x73, 0x74, 0x02, 0x00, 0x00, 0x03, 0x2b, 0x00, 0x54, 0x68, 0x65, 0x20, 0x4d, 0x6f, 0x74, 0x68,
0x20, 0x50, 0x6f, 0x64, 0x63, 0x61, 0x73, 0x74, 0x3a, 0x20, 0x4d, 0x6f, 0x6e, 0x69, 0x63, 0x61,
0x20, 0x43, 0x68, 0x65, 0x72, 0x75, 0x2d, 0x4d, 0x70, 0x61, 0x6d, 0x62, 0x61, 0x77, 0x61, 0x73,
0xe2, 0x80, 0xa6, 0x04, 0x03, 0x00, 0x31, 0x31, 0x33,
/* adding an app_id because it didnt' come with one */
0x00, 0x01, 0x00, 0x50,
};
//! Complete dict for a messages app notification
//! 00 1a 00 00 00 06 00 00 07 05 00 43 6c 65 61 72 ........ ...Clear
//! 00 13 00 63 6f 6d 2e 61 70 70 6c 65 2e 4d 6f 62 ...com.a pple.Mob
//! 69 6c 65 53 4d 53 01 13 00 46 72 61 6e 63 6f 69 ileSMS.. .Francoi
//! 73 20 42 61 6c 64 61 73 73 61 72 69 02 00 00 03 s Baldas sari....
//! 1b 00 74 68 69 73 20 69 73 20 61 20 74 65 73 74 ..this i s a test
//! 20 6e 6f 74 69 66 69 63 61 74 69 6f 6e 05 0f 00 notific ation...
//! 32 30 31 35 30 36 31 35 54 31 36 31 38 33 37 20150615 T161837
static const uint8_t s_message_dict[] = {
0x00, 0x1a, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0x05, 0x00, 0x43, 0x6c, 0x65, 0x61, 0x72,
0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x4d, 0x6f, 0x62,
0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x13, 0x00, 0x46, 0x72, 0x61, 0x6e, 0x63, 0x6f, 0x69,
0x73, 0x20, 0x42, 0x61, 0x6c, 0x64, 0x61, 0x73, 0x73, 0x61, 0x72, 0x69, 0x02, 0x00, 0x00, 0x03,
0x1b, 0x00, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x74, 0x65, 0x73, 0x74,
0x20, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x05, 0x0f, 0x00,
0x32, 0x30, 0x31, 0x35, 0x30, 0x36, 0x31, 0x35, 0x54, 0x31, 0x36, 0x31, 0x38, 0x33, 0x37
};
static TimelineItem s_message_parsed_item = {
.header = {
.id = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.ancs_uid = 0x1a,
.timestamp = 1434385117,
.duration = 0,
.type = TimelineItemTypeNotification,
.layout = LayoutIdNotification,
.ancs_notif = 1
},
.attr_list = (AttributeList) {
.num_attributes = 6,
.attributes = (Attribute[6]) {
{ .id = AttributeIdSender, .cstring = "Francois Baldassari" },
{ .id = AttributeIdTitle, .cstring = "Francois Baldassari" },
{ .id = AttributeIdBody, .cstring = "this is a test notification" },
{ .id = AttributeIdiOSAppIdentifier, .cstring = "com.apple.MobileSMS" },
{ .id = AttributeIdIconTiny, .uint32 = TIMELINE_RESOURCE_GENERIC_SMS },
{ .id = AttributeIdBgColor, .uint8 = GColorIslamicGreenARGB8 }
}
},
.action_group = (TimelineItemActionGroup) {
.num_actions = 1,
.actions = (TimelineItemAction[1]) {
{
.id = 0,
.type = TimelineItemActionTypeAncsNegative,
.attr_list = (AttributeList) {
.num_attributes = 2,
.attributes = (Attribute[2]) {
{ .id = AttributeIdAncsAction, .uint8 = ActionIDNegative},
{ .id = AttributeIdTitle, .cstring = "Dismiss" }}
}
}
}
}
};
//! Complete dict for the messages app attributes
//! 01 63 6f 6d 2e 61 70 70 6c 65 2e 4d 6f 62 69 6c .com.app le.Mobil
//! 65 53 4d 53 00 00 08 00 4d 65 73 73 61 67 65 73 eSMS.... Messages
static const uint8_t s_message_app_info_dict[] = {
0x01, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x4d, 0x6f, 0x62, 0x69, 0x6c,
0x65, 0x53, 0x4d, 0x53, 0x00, 0x00, 0x08, 0x00, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73
};
//! Complete dict for the unrecognized fake app attributes
//! 01 63 6f 6d 2e 74 65 73 74 73 2e 4e 6f 74 41 6e .com.tes ts.NotAn
//! 41 70 70 00 00 07 00 46 61 6b 65 41 70 70 App....F akeApp
static const uint8_t s_fake_app_info_dict[] = {
0x01, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x4e, 0x6f, 0x74, 0x41, 0x6e,
0x41, 0x70, 0x70, 0x00, 0x00, 0x07, 0x00, 0x46, 0x61, 0x6b, 0x65, 0x41, 0x70, 0x70,
};
// Complete app dictionary sent by ANCS
// 01 63 6f 6d 2e 67 65 74 70 65 62 62 6c 65 2e 70 .com.get pebble.p
// 65 62 62 6c 65 74 69 6d 65 2e 65 6e 74 65 72 70 ebbletim e.enterp
// 72 69 73 65 00 00 0e 00 50 65 62 62 6c 65 20 54 rise.... Pebble T
// 69 6d 65 20 ce b2 ime ..
static const uint8_t s_app_attr_dict[] = {
0x01, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x65, 0x74, 0x70, 0x65, 0x62, 0x62, 0x6c, 0x65, 0x2e, 0x70,
0x65, 0x62, 0x62, 0x6c, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70,
0x72, 0x69, 0x73, 0x65, 0x00, 0x00, 0x0e, 0x00, 0x50, 0x65, 0x62, 0x62, 0x6c, 0x65, 0x20, 0x54,
0x69, 0x6d, 0x65, 0x20, 0xce, 0xb2
};
static const uint8_t s_missing_last_attribute[] = {
0x00, 0x95, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c,
0x65, 0x2e, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x10, 0x00, 0x72, 0x6a,
0x63, 0x61, 0x73, 0x65, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x02, 0x00,
0x00, 0x03, 0x26, 0x00, 0x43, 0x61, 0x72, 0x65, 0x66, 0x75, 0x6c, 0x2c, 0x20, 0x6d, 0x61, 0x6e,
0x2c, 0x20, 0x74, 0x68, 0x65, 0x72, 0x65, 0x27, 0x73, 0x20, 0x61, 0x20, 0x62, 0x65, 0x76, 0x65,
0x72, 0x61, 0x67, 0x65, 0x20, 0x68, 0x65, 0x72, 0x65, 0x21
};
// Length of attribute too long
static const uint8_t s_invalid_attribute_length[] = {
0x00, 0x94, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c,
0x65, 0x2e, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x29, 0x00, 0x72, 0x6a,
0x63, 0x61, 0x73, 0x65, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x43, 0x61, 0x72, 0x65, 0x66, 0x75, 0x6c, 0x2c, 0x20, 0x6d, 0x61, 0x6e,
0x2c, 0x20, 0x74, 0x68, 0x65, 0x72, 0x65, 0x00, 0x73, 0x20, 0x61, 0x20, 0x62, 0x65, 0x76, 0x65,
0x72, 0x61, 0x67, 0x65, 0x20, 0x68, 0x65, 0x72, 0x65, 0x21, 0x02, 0x00, 0x00, 0x03, 0x26, 0x00,
0x43, 0x61, 0x72, 0x65, 0x66, 0x75, 0x6c, 0x2c, 0x20, 0x6d, 0x61, 0x6e, 0x2c, 0x20, 0x74, 0x68,
0x65, 0x72, 0x65, 0x27, 0x73, 0x20, 0x61, 0x20, 0x62, 0x65, 0x76, 0x65, 0x72, 0x61, 0x67, 0x65,
0x20, 0x68, 0x65, 0x72, 0x65, 0x21, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x33, 0x31, 0x31, 0x30,
0x35, 0x54, 0x31, 0x38, 0x31, 0x39, 0x32, 0x38
};
// Chunked dictionary 1/2 sent by ANCS (w/header)
// 00 0d 00 00 00 00 1b 00 63 6f 6d 2e 6b 69 63 6b ........ com.kick
// 73 74 61 72 74 65 72 2e 6b 69 63 6b 73 74 61 72 starter. kickstar
// 74 65 72 01 0b 00 4b 69 63 6b 73 74 61 72 74 65 ter...Ki ckstarte
// 72 02 00 00 03 56 00 52 65 73 70 6f 6e 73 65 20 r....V.R esponse
// 6e 65 65 64 65 64 21 20 47 65 74 20 79 6f 75 72 needed! Get your
// 20 72 65 77 61 72 64 20 66 6f 72 20 62 61 63 6b reward for back
// 69 6e 67 20 50 65 62 62 6c 65 3a 20 45 2d 50 61 ing Pebb le: E-Pa
// 70 65 72 79 20 57 61 74 63 68 20 66 6f 72 20 69 pery Wat ch for i
// 4f 53 20 61 6e 64 20 44 72 6f 69 64 2e 05 0f 00 OS and D roid....
// 32 30 31 34 30 36 31 30 54 32 32 20140610 T22
static const uint8_t s_split_timestamp_dict_part_one[] = {
0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x6b, 0x69, 0x63, 0x6b,
0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x72, 0x2e, 0x6b, 0x69, 0x63, 0x6b, 0x73, 0x74, 0x61, 0x72,
0x74, 0x65, 0x72, 0x01, 0x0b, 0x00, 0x4b, 0x69, 0x63, 0x6b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65,
0x72, 0x02, 0x00, 0x00, 0x03, 0x56, 0x00, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x20,
0x6e, 0x65, 0x65, 0x64, 0x65, 0x64, 0x21, 0x20, 0x47, 0x65, 0x74, 0x20, 0x79, 0x6f, 0x75, 0x72,
0x20, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x62, 0x61, 0x63, 0x6b,
0x69, 0x6e, 0x67, 0x20, 0x50, 0x65, 0x62, 0x62, 0x6c, 0x65, 0x3a, 0x20, 0x45, 0x2d, 0x50, 0x61,
0x70, 0x65, 0x72, 0x79, 0x20, 0x57, 0x61, 0x74, 0x63, 0x68, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x69,
0x4f, 0x53, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x44, 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x05, 0x0f, 0x00,
0x32, 0x30, 0x31, 0x34, 0x30, 0x36, 0x31, 0x30, 0x54, 0x32, 0x32
};
// Chunked dictionary 2/2 sent by ANCS
// 32 30 34 38 2048
static const uint8_t s_split_timestamp_dict_part_two[] = {
0x32, 0x30, 0x34, 0x38
};
static const uint8_t s_invalid_attribute_length_fixed[] = {
0x00, 0x94, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c,
0x65, 0x2e, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x10, 0x00, 0x72, 0x6a,
0x63, 0x61, 0x73, 0x65, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x02, 0x00,
0x00, 0x03, 0x26, 0x00, 0x43, 0x61, 0x72, 0x65, 0x66, 0x75, 0x6c, 0x2c, 0x20, 0x6d, 0x61, 0x6e,
0x2c, 0x20, 0x74, 0x68, 0x65, 0x72, 0x65, 0x27, 0x73, 0x20, 0x61, 0x20, 0x62, 0x65, 0x76, 0x65,
0x72, 0x61, 0x67, 0x65, 0x20, 0x68, 0x65, 0x72, 0x65, 0x21, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31,
0x33, 0x31, 0x31, 0x30, 0x35, 0x54, 0x31, 0x38, 0x31, 0x39, 0x32, 0x38
};
// 00 22 00 00 00 07 07 00 44 69 73 6d 69 73 73 00 ."...... Dismiss.
// 14 00 63 6f 6d 2e 61 70 70 6c 65 2e 6d 6f 62 69 ..com.ap ple.mobi
// 6c 65 6d 61 69 6c 01 15 00 41 61 72 6f 6e 20 48 lemail.. .Aaron H
// 65 63 6b 6d 61 6e 6e 20 28 4a 49 52 41 29 02 28 eckmann (JIRA).(
// 00 5b 4a 49 52 41 5d 20 28 50 42 4c 2d 38 37 37 .[JIRA] (PBL-877
// 36 29 20 45 72 72 6f 72 20 73 63 72 65 65 6e 20 6) Error screen
// 61 66 74 65 72 20 6c 6f 67 03 1c 00 54 68 69 73 after lo g...This
// 20 6d 65 73 73 61 67 65 20 68 61 73 20 6e 6f 20 message has no
// 63 6f 6e 74 65 6e 74 2e 05 0f 00 32 30 31 34 30 content. ...20140
// 39 32 36 54 31 30 34 33 30 30 926T1043 00
static const uint8_t s_this_message_has_no_content_response[] = {
0x00, 0x22, 0x00, 0x00, 0x00, 0x07, 0x07, 0x00, 0x44, 0x69, 0x73, 0x6d, 0x69, 0x73, 0x73, 0x00,
0x14, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x62, 0x69,
0x6c, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x01, 0x15, 0x00, 0x41, 0x61, 0x72, 0x6f, 0x6e, 0x20, 0x48,
0x65, 0x63, 0x6b, 0x6d, 0x61, 0x6e, 0x6e, 0x20, 0x28, 0x4a, 0x49, 0x52, 0x41, 0x29, 0x02, 0x28,
0x00, 0x5b, 0x4a, 0x49, 0x52, 0x41, 0x5d, 0x20, 0x28, 0x50, 0x42, 0x4c, 0x2d, 0x38, 0x37, 0x37,
0x36, 0x29, 0x20, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x20, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x20,
0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x6c, 0x6f, 0x67, 0x03, 0x1c, 0x00, 0x54, 0x68, 0x69, 0x73,
0x20, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x20, 0x68, 0x61, 0x73, 0x20, 0x6e, 0x6f, 0x20,
0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2e, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x34, 0x30,
0x39, 0x32, 0x36, 0x54, 0x31, 0x30, 0x34, 0x33, 0x30, 0x30
};
// 00 19 00 00 00 07 07 00 44 69 73 6d 69 73 73 00 ........ Dismiss.
// 14 00 63 6f 6d 2e 61 70 70 6c 65 2e 6d 6f 62 69 ..com.ap ple.mobi
// 6c 65 6d 61 69 6c 01 05 00 4d 61 72 74 79 02 04 lemail.. .Marty..
// 00 74 65 73 74 03 0a 00 4c 6f 61 64 69 6e 67 e2 .test... Loading.
// 80 a6 05 0f 00 32 30 31 34 30 39 32 39 54 31 35 .....201 40929T15
// 31 35 31 30 1510
static const uint8_t s_loading_response[] = {
0x00, 0x19, 0x00, 0x00, 0x00, 0x07, 0x07, 0x00, 0x44, 0x69, 0x73, 0x6d, 0x69, 0x73, 0x73, 0x00,
0x14, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x62, 0x69,
0x6c, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x01, 0x05, 0x00, 0x4d, 0x61, 0x72, 0x74, 0x79, 0x02, 0x04,
0x00, 0x74, 0x65, 0x73, 0x74, 0x03, 0x0a, 0x00, 0x4c, 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, 0xe2,
0x80, 0xa6, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x34, 0x30, 0x39, 0x32, 0x39, 0x54, 0x31, 0x35,
0x31, 0x35, 0x31, 0x30,
};
// 00 aa 00 00 00 07 07 00 44 69 73 6d 69 73 73 00 ........ Dismiss.
// 14 00 63 6f 6d 2e 61 70 70 6c 65 2e 6d 6f 62 69 ..com.ap ple.mobi
// 6c 65 6d 61 69 6c 01 16 00 63 61 6c 65 6e 64 61 lemail.. .calenda
// 72 40 63 73 61 69 6c 2e 6d 69 74 2e 65 64 75 02 r@csail. mit.edu.
// 28 00 54 41 4c 4b 3a 20 54 75 65 73 64 61 79 20 (.TALK: Tuesday
// 31 30 2d 32 38 2d 32 30 31 34 20 53 63 69 65 6e 10-28-20 14 Scien
// 74 69 66 69 63 20 55 74 6f 70 03 c8 00 53 63 69 tific Ut op...Sci
// 65 6e 74 69 66 69 63 20 55 74 6f 70 69 61 3a 20 entific Utopia:
// 49 6d 70 72 6f 76 69 6e 67 20 4f 70 65 6e 6e 65 Improvin g Openne
// 73 73 20 61 6e 64 20 52 65 70 72 6f 64 75 63 69 ss and R eproduci
// 62 69 6c 69 74 79 20 69 6e 20 53 63 69 65 6e 74 bility i n Scient
// 69 66 69 63 20 52 65 73 65 61 72 63 68 20 53 70 ific Res earch Sp
// 65 61 6b 65 72 3a 20 42 72 69 61 6e 20 4e 6f 73 eaker: B rian Nos
// 65 6b 2c 20 55 6e 69 76 65 72 73 69 74 79 20 6f ek, Univ ersity o
// 66 20 56 69 72 67 69 6e 69 61 20 61 6e 64 20 43 f Virgin ia and C
// 65 6e 74 65 72 20 66 6f 72 20 4f 70 65 6e 20 53 enter fo r Open S
// 63 69 65 6e 63 65 20 53 70 65 61 6b 65 72 20 41 cience S peaker A
// 66 66 69 6c 69 61 74 69 6f 6e 3a 20 55 6e 69 76 ffiliati on: Univ
// 65 72 73 69 74 79 20 6f 66 20 56 69 72 67 69 6e ersity o f Virgin
// 69 61 20 48 6f 05 ia Ho.
typedef struct PACKED s_sample_memory_layout {
const uint8_t attribute_data[310];
const uint8_t out_of_bounds_memory[6];
} s_sample_memory_layout;
s_sample_memory_layout memory_with_attribute_id_at_end = {
.attribute_data = {
0x00, 0xaa, 0x00, 0x00, 0x00, 0x07, 0x07, 0x00, 0x44, 0x69, 0x73, 0x6d, 0x69, 0x73, 0x73, 0x00,
0x14, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x62, 0x69,
0x6c, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x01, 0x16, 0x00, 0x63, 0x61, 0x6c, 0x65, 0x6e, 0x64, 0x61,
0x72, 0x40, 0x63, 0x73, 0x61, 0x69, 0x6c, 0x2e, 0x6d, 0x69, 0x74, 0x2e, 0x65, 0x64, 0x75, 0x02,
0x28, 0x00, 0x54, 0x41, 0x4c, 0x4b, 0x3a, 0x20, 0x54, 0x75, 0x65, 0x73, 0x64, 0x61, 0x79, 0x20,
0x31, 0x30, 0x2d, 0x32, 0x38, 0x2d, 0x32, 0x30, 0x31, 0x34, 0x20, 0x53, 0x63, 0x69, 0x65, 0x6e,
0x74, 0x69, 0x66, 0x69, 0x63, 0x20, 0x55, 0x74, 0x6f, 0x70, 0x03, 0xc8, 0x00, 0x53, 0x63, 0x69,
0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x63, 0x20, 0x55, 0x74, 0x6f, 0x70, 0x69, 0x61, 0x3a, 0x20,
0x49, 0x6d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x6e, 0x67, 0x20, 0x4f, 0x70, 0x65, 0x6e, 0x6e, 0x65,
0x73, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x52, 0x65, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x69,
0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x20, 0x69, 0x6e, 0x20, 0x53, 0x63, 0x69, 0x65, 0x6e, 0x74,
0x69, 0x66, 0x69, 0x63, 0x20, 0x52, 0x65, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x20, 0x53, 0x70,
0x65, 0x61, 0x6b, 0x65, 0x72, 0x3a, 0x20, 0x42, 0x72, 0x69, 0x61, 0x6e, 0x20, 0x4e, 0x6f, 0x73,
0x65, 0x6b, 0x2c, 0x20, 0x55, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x69, 0x74, 0x79, 0x20, 0x6f,
0x66, 0x20, 0x56, 0x69, 0x72, 0x67, 0x69, 0x6e, 0x69, 0x61, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x43,
0x65, 0x6e, 0x74, 0x65, 0x72, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x4f, 0x70, 0x65, 0x6e, 0x20, 0x53,
0x63, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x20, 0x53, 0x70, 0x65, 0x61, 0x6b, 0x65, 0x72, 0x20, 0x41,
0x66, 0x66, 0x69, 0x6c, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x55, 0x6e, 0x69, 0x76,
0x65, 0x72, 0x73, 0x69, 0x74, 0x79, 0x20, 0x6f, 0x66, 0x20, 0x56, 0x69, 0x72, 0x67, 0x69, 0x6e,
0x69, 0x61, 0x20, 0x48, 0x6f, 0x05,
},
.out_of_bounds_memory = {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
},
};
static const uint8_t memory_with_attribute_id_at_end_p2[] = {
0x0f, 0x00, 0x32, 0x30, 0x31, 0x33, 0x31, 0x31, 0x30, 0x35, 0x54, 0x31, 0x38, 0x31, 0x39, 0x32, 0x38
};
// 00 74 00 00 00 06 06 00 41 6e 73 77 65 72 07 07 .t...... Answer..
// 00 44 65 63 6c 69 6e 65 00 15 00 63 6f 6d 2e 61 .Decline ...com.a
// 70 70 6c 65 2e 6d 6f 62 69 6c 65 70 68 6f 6e 65 pple.mob ilephone
// 01 03 00 49 6d 6d 02 06 00 6d 6f 62 69 6c 65 03 ...Imm.. .mobile.
// 0d 00 49 6e 63 6f 6d 69 6e 67 20 43 61 6c 6c 05 ..Incomi ng Call.
// 00 00 00 75 00 00 00 07 07 00 44 69 73 6d 69 73 ...u.... ..Dismis
// 73 00 13 00 63 6f 6d 2e 61 70 70 6c 65 2e 6d 6f s...com. apple.mo
// 62 69 6c 65 63 61 6c 01 11 00 48 61 69 6c 65 79 bilecal. ..Hailey
// 27 73 20 70 72 61 63 74 69 63 65 02 00 00 03 0a 's pract ice.....
// 00 52 65 64 20 4d 6f 72 74 6f 6e 05 0f 00 32 30 .Red Mor ton...20
// 31 34 31 30 32 38 54 31 36 30 30 30 30 00 76 00 141028T1 60000.v.
// 00 00 06 06 00 41 6e 73 77 65 72 07 07 00 44 65 .....Ans wer...De
// 63 6c 69 6e 65 00 15 00 63 6f 6d 2e 61 70 70 6c cline... com.appl
// 65 2e 6d 6f 62 69 6c 65 70 68 6f 6e 58 01 03 00 e.mobile phonX... //phone -> phonX for testing
// 49 6d 6d 02 06 00 6d 6f 62 69 6c 65 03 0d 00 49 Imm...mo bile...I
// 6e 63 6f 6d 69 6e 67 20 43 61 6c 6c 05 00 00 ncoming Call...
static const uint8_t s_multiple_complete_dicts[] = {
// Dict 1
0x00, 0x74, 0x00, 0x00, 0x00, 0x06, 0x06, 0x00, 0x41, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x07, 0x07,
0x00, 0x44, 0x65, 0x63, 0x6c, 0x69, 0x6e, 0x65, 0x00, 0x15, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61,
0x70, 0x70, 0x6c, 0x65, 0x2e, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x70, 0x68, 0x6f, 0x6e, 0x65,
0x01, 0x03, 0x00, 0x49, 0x6d, 0x6d, 0x02, 0x06, 0x00, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x03,
0x0d, 0x00, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x20, 0x43, 0x61, 0x6c, 0x6c, 0x05,
0x00, 0x00,
// Dict 2
0x00, 0x75, 0x00, 0x00, 0x00, 0x07, 0x07, 0x00, 0x44, 0x69, 0x73, 0x6d, 0x69, 0x73,
0x73, 0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x6d, 0x6f,
0x62, 0x69, 0x6c, 0x65, 0x63, 0x61, 0x6c, 0x01, 0x11, 0x00, 0x48, 0x61, 0x69, 0x6c, 0x65, 0x79,
0x27, 0x73, 0x20, 0x70, 0x72, 0x61, 0x63, 0x74, 0x69, 0x63, 0x65, 0x02, 0x00, 0x00, 0x03, 0x0a,
0x00, 0x52, 0x65, 0x64, 0x20, 0x4d, 0x6f, 0x72, 0x74, 0x6f, 0x6e, 0x05, 0x0f, 0x00, 0x32, 0x30,
0x31, 0x34, 0x31, 0x30, 0x32, 0x38, 0x54, 0x31, 0x36, 0x30, 0x30, 0x30, 0x30,
//Dict 3
0x00, 0x76, 0x00,
0x00, 0x00, 0x06, 0x06, 0x00, 0x41, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x07, 0x07, 0x00, 0x44, 0x65,
0x63, 0x6c, 0x69, 0x6e, 0x65, 0x00, 0x15, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c,
0x65, 0x2e, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x70, 0x68, 0x6f, 0x6e, 0x64, 0x01, 0x03, 0x00,
0x49, 0x6d, 0x6d, 0x02, 0x06, 0x00, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x03, 0x0d, 0x00, 0x49,
0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x20, 0x43, 0x61, 0x6c, 0x6c, 0x05, 0x00, 0x00
};
//! Complete dict for a messages app notification with a duplicated title
//! 00 1b 00 00 00 06 00 00 07 05 00 43 6c 65 61 72 ........ ...Clear
//! 00 13 00 63 6f 6d 2e 61 70 70 6c 65 2e 4d 6f 62 ...com.a pple.Mob
//! 69 6c 65 53 4d 53 01 08 00 4D 65 73 73 61 67 65 ileSMS.. .Message
//! 73 02 00 00 03 1b 00 74 68 69 73 20 69 73 20 61 s......t his is a
//! 20 74 65 73 74 20 6e 6f 74 69 66 69 63 61 74 69 test no tificati
//! 6f 6e 05 0f 00 32 30 31 35 30 36 31 35 54 31 36 on...201 50615T16
//! 31 38 33 37 1837
static const uint8_t s_app_name_title_dict[] = {
0x00, 0x1b, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0x05, 0x00, 0x43, 0x6c, 0x65, 0x61, 0x72,
0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x4d, 0x6f, 0x62,
0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x08, 0x00, 0x4D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x73, 0x02, 0x00, 0x00, 0x03, 0x1b, 0x00, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61,
0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x35, 0x30, 0x36, 0x31, 0x35, 0x54, 0x31, 0x36,
0x31, 0x38, 0x33, 0x37
};
static TimelineItem s_app_name_title_parsed_item = {
.header = {
.id = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.ancs_uid = 0x1b,
.timestamp = 1434385117,
.duration = 0,
.type = TimelineItemTypeNotification,
.layout = LayoutIdNotification,
.ancs_notif = 1
},
.attr_list = (AttributeList) {
.num_attributes = 4,
.attributes = (Attribute[4]) {
{ .id = AttributeIdBody, .cstring = "this is a test notification" },
{ .id = AttributeIdiOSAppIdentifier, .cstring = "com.apple.MobileSMS" },
{ .id = AttributeIdIconTiny, .uint32 = TIMELINE_RESOURCE_GENERIC_SMS },
{ .id = AttributeIdBgColor, .uint8 = GColorIslamicGreenARGB8 },
}
},
.action_group = (TimelineItemActionGroup) {
.num_actions = 1,
.actions = (TimelineItemAction[1]) {
{
.id = 0,
.type = TimelineItemActionTypeAncsNegative,
.attr_list = (AttributeList) {
.num_attributes = 2,
.attributes = (Attribute[2]) {
{ .id = AttributeIdAncsAction, .uint8 = ActionIDNegative},
{ .id = AttributeIdTitle, .cstring = "Dismiss" }}
}
}
}
}
};
//! Complete dict for a notification from an unrecognized app with a duplicated title
//! 00 1c 00 00 00 06 04 00 4c 69 6b 65 07 05 00 43 ........ Like...C
//! 6c 65 61 72 00 12 00 63 6f 6d 2e 74 65 73 74 73 lear...com.t ests
//! 2e 4e 6f 74 41 6e 41 70 70 01 07 00 46 61 6b 65 .NotAnApp... Fake
//! 41 70 70 02 00 00 03 1b 00 74 68 69 73 20 69 73 App......thi s is
//! 20 61 20 74 65 73 74 20 6e 6f 74 69 66 69 63 61 a test noti fica
//! 74 69 6f 6e 05 0f 00 32 30 31 35 30 36 31 35 54 tion...20150 615T
//! 31 36 31 38 33 37 161837
static const uint8_t s_unknown_app_dict[] = {
0x00, 0x1c, 0x00, 0x00, 0x00, 0x06, 0x04, 0x00, 0x4c, 0x69, 0x6b, 0x65, 0x07, 0x05, 0x00, 0x43,
0x6c, 0x65, 0x61, 0x72, 0x00, 0x12, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73,
0x2e, 0x4e, 0x6f, 0x74, 0x41, 0x6e, 0x41, 0x70, 0x70, 0x01, 0x07, 0x00, 0x46, 0x61, 0x6b, 0x65,
0x41, 0x70, 0x70, 0x02, 0x00, 0x00, 0x03, 0x1b, 0x00, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73,
0x20, 0x61, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x35, 0x30, 0x36, 0x31, 0x35, 0x54,
0x31, 0x36, 0x31, 0x38, 0x33, 0x37
};
static TimelineItem s_unknown_app_parsed_item = {
.header = {
.id = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.ancs_uid = 0x1c,
.timestamp = 1434385117,
.duration = 0,
.type = TimelineItemTypeNotification,
.layout = LayoutIdNotification,
.ancs_notif = 1
},
.attr_list = (AttributeList) {
.num_attributes = 4,
.attributes = (Attribute[4]) {
{ .id = AttributeIdAppName, .cstring = "FakeApp" },
{ .id = AttributeIdBody, .cstring = "this is a test notification" },
{ .id = AttributeIdiOSAppIdentifier, .cstring = "com.tests.NotAnApp" },
{ .id = AttributeIdIconTiny, .uint32 = TIMELINE_RESOURCE_NOTIFICATION_GENERIC },
}
},
.action_group = (TimelineItemActionGroup) {
.num_actions = 2,
.actions = (TimelineItemAction[2]) {
{
.id = 0,
.type = TimelineItemActionTypeAncsPositive,
.attr_list = (AttributeList) {
.num_attributes = 2,
.attributes = (Attribute[2]) {
{ .id = AttributeIdAncsAction, .uint8 = ActionIDPositive},
{ .id = AttributeIdTitle, .cstring = "Like" }}
}
},
{
.id = 0,
.type = TimelineItemActionTypeAncsNegative,
.attr_list = (AttributeList) {
.num_attributes = 2,
.attributes = (Attribute[2]) {
{ .id = AttributeIdAncsAction, .uint8 = ActionIDNegative},
{ .id = AttributeIdTitle, .cstring = "Dismiss" }}
}
}
}
}
};
//! Complete dict for a notification from an unrecognized app with a unique title
//! 00 1d 00 00 00 06 00 00 07 05 00 43 6c 65 61 72 ........ ...Clear
//! 00 12 00 63 6f 6d 2e 74 65 73 74 73 2e 4e 6f 74 ...com.t ests.Not
//! 41 6e 41 70 70 01 0d 00 4d 65 73 73 61 67 65 20 AnApp... Message
//! 54 69 74 6c 65 02 00 00 03 1b 00 74 68 69 73 20 Title... ...this
//! 69 73 20 61 20 74 65 73 74 20 6e 6f 74 69 66 69 is a tes t notifi
//! 63 61 74 69 6f 6e 05 0f 00 32 30 31 35 30 36 31 cation.. .2015061
//! 35 54 31 36 31 38 33 37 5T161837
static const uint8_t s_unknown_app_unique_title_dict[] = {
0x00, 0x1d, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0x05, 0x00, 0x43, 0x6c, 0x65, 0x61, 0x72,
0x00, 0x12, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x4e, 0x6f, 0x74,
0x41, 0x6e, 0x41, 0x70, 0x70, 0x01, 0x0d, 0x00, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x20,
0x54, 0x69, 0x74, 0x6c, 0x65, 0x02, 0x00, 0x00, 0x03, 0x1b, 0x00, 0x74, 0x68, 0x69, 0x73, 0x20,
0x69, 0x73, 0x20, 0x61, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69,
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x35, 0x30, 0x36, 0x31,
0x35, 0x54, 0x31, 0x36, 0x31, 0x38, 0x33, 0x37
};
static TimelineItem s_unknown_app_unique_title_parsed_item = {
.header = {
.id = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.ancs_uid = 0x1d,
.timestamp = 1434385117,
.duration = 0,
.type = TimelineItemTypeNotification,
.layout = LayoutIdNotification,
.ancs_notif = 1
},
.attr_list = (AttributeList) {
.num_attributes = 5,
.attributes = (Attribute[5]) {
{ .id = AttributeIdTitle, .cstring = "Message Title" },
{ .id = AttributeIdAppName, .cstring = "FakeApp" },
{ .id = AttributeIdBody, .cstring = "this is a test notification" },
{ .id = AttributeIdiOSAppIdentifier, .cstring = "com.tests.NotAnApp" },
{ .id = AttributeIdIconTiny, .uint32 = TIMELINE_RESOURCE_NOTIFICATION_GENERIC },
}
},
.action_group = (TimelineItemActionGroup) {
.num_actions = 1,
.actions = (TimelineItemAction[1]) {
{
.id = 0,
.type = TimelineItemActionTypeAncsNegative,
.attr_list = (AttributeList) {
.num_attributes = 2,
.attributes = (Attribute[2]) {
{ .id = AttributeIdAncsAction, .uint8 = ActionIDNegative},
{ .id = AttributeIdTitle, .cstring = "Dismiss" }}
}
}
}
}
};
//! Complete dict for an MMS with an image
//! 00 1f 00 00 00 06 00 00 07 05 00 43 6c 65 61 72 .4.........Clear
//! 00 13 00 63 6f 6d 2e 61 70 70 6c 65 2e 4d 6f 62 ...com.apple.Mob
//! 69 6c 65 53 4d 53 01 08 00 48 75 79 20 54 72 61 ileSMS...Huy Tra
//! 6e 02 00 00 03 00 00 05 0f 00 32 30 31 36 30 31 n.........201601
//! 31 31 54 31 38 34 38 35 32 11T184852
static const uint8_t s_mms_no_caption_dict[] = {
0x00, 0x1f, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0x05, 0x00, 0x43, 0x6c, 0x65, 0x61, 0x72,
0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x4d, 0x6f, 0x62,
0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x08, 0x00, 0x48, 0x75, 0x79, 0x20, 0x54, 0x72, 0x61,
0x6e, 0x02, 0x00, 0x00, 0x03, 0x00, 0x00, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x36, 0x30, 0x31,
0x31, 0x31, 0x54, 0x31, 0x38, 0x34, 0x38, 0x35, 0x32
};
static TimelineItem s_mms_no_caption_parsed_item = {
.header = {
.id = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.ancs_uid = 0x1f,
.timestamp = 1452538132,
.duration = 0,
.type = TimelineItemTypeNotification,
.layout = LayoutIdNotification,
.ancs_notif = 1
},
.attr_list = (AttributeList) {
.num_attributes = 5,
.attributes = (Attribute[5]) {
{ .id = AttributeIdSender, .cstring = "Huy Tran" },
{ .id = AttributeIdBody, .cstring = "Huy Tran sent an attachment 🎁" },
{ .id = AttributeIdiOSAppIdentifier, .cstring = "com.apple.MobileSMS" },
{ .id = AttributeIdIconTiny, .uint32 = TIMELINE_RESOURCE_GENERIC_SMS },
{ .id = AttributeIdBgColor, .uint8 = GColorIslamicGreenARGB8 }
}
},
.action_group = (TimelineItemActionGroup) {
.num_actions = 1,
.actions = (TimelineItemAction[1]) {
{
.id = 0,
.type = TimelineItemActionTypeAncsNegative,
.attr_list = (AttributeList) {
.num_attributes = 2,
.attributes = (Attribute[2]) {
{ .id = AttributeIdAncsAction, .uint8 = ActionIDNegative },
{ .id = AttributeIdTitle, .cstring = "Dismiss" }
}
}
}
}
}
};
//! Complete dict for an MMS with an image and caption
//! 00 20 00 00 00 06 00 00 07 05 00 43 6c 65 61 72 .'...... ...Clear
//! 00 13 00 63 6f 6d 2e 61 70 70 6c 65 2e 4d 6f 62 ...com.a pple.Mob
//! 69 6c 65 53 4d 53 01 08 00 48 75 79 20 54 72 61 ileSMS.. .Huy Tra
//! 6e 02 00 00 03 10 00 20 43 68 65 63 6b 20 74 68 n...... Check th
//! 69 73 20 6f 75 74 21 05 0f 00 32 30 31 36 30 31 is out!. ..201601
//! 31 31 54 32 31 30 34 31 35 11T21041 5
static const uint8_t s_mms_with_caption_dict[] = {
0x00, 0x20, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0x05, 0x00, 0x43, 0x6c, 0x65, 0x61, 0x72,
0x00, 0x13, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x4d, 0x6f, 0x62,
0x69, 0x6c, 0x65, 0x53, 0x4d, 0x53, 0x01, 0x08, 0x00, 0x48, 0x75, 0x79, 0x20, 0x54, 0x72, 0x61,
0x6e, 0x02, 0x00, 0x00, 0x03, 0x10, 0x00, 0x20, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x20, 0x74, 0x68,
0x69, 0x73, 0x20, 0x6f, 0x75, 0x74, 0x21, 0x05, 0x0f, 0x00, 0x32, 0x30, 0x31, 0x36, 0x30, 0x31,
0x31, 0x31, 0x54, 0x32, 0x31, 0x30, 0x34, 0x31, 0x35
};
static TimelineItem s_mms_with_caption_parsed_item = {
.header = {
.id = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.ancs_uid = 0x20,
.timestamp = 1452546255,
.duration = 0,
.type = TimelineItemTypeNotification,
.layout = LayoutIdNotification,
.ancs_notif = 1
},
.attr_list = (AttributeList) {
.num_attributes = 5,
.attributes = (Attribute[5]) {
{ .id = AttributeIdSender, .cstring = "Huy Tran" },
{ .id = AttributeIdBody, .cstring = "Huy Tran sent an attachment 🎁\n\"Check this out!\"" },
{ .id = AttributeIdiOSAppIdentifier, .cstring = "com.apple.MobileSMS" },
{ .id = AttributeIdIconTiny, .uint32 = TIMELINE_RESOURCE_GENERIC_SMS },
{ .id = AttributeIdBgColor, .uint8 = GColorIslamicGreenARGB8 }
}
},
.action_group = (TimelineItemActionGroup) {
.num_actions = 1,
.actions = (TimelineItemAction[1]) {
{
.id = 0,
.type = TimelineItemActionTypeAncsNegative,
.attr_list = (AttributeList) {
.num_attributes = 2,
.attributes = (Attribute[2]) {
{ .id = AttributeIdAncsAction, .uint8 = ActionIDNegative },
{ .id = AttributeIdTitle, .cstring = "Dismiss" }
}
}
}
}
}
};
+720
View File
@@ -0,0 +1,720 @@
/*
* 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 "comm/ble/kernel_le_client/ams/ams.h"
#include "comm/bt_conn_mgr.h"
#include "services/normal/music.h"
#include "services/normal/music_internal.h"
#include "clar.h"
// Stubs & Fakes
///////////////////////////////////////////////////////////
#include "fake_events.h"
#include "fake_gatt_client_operations.h"
#include "fake_gatt_client_subscriptions.h"
#include "fake_pebble_tasks.h"
#include "fake_rtc.h"
#include "stubs_app_install_manager.h"
#include "stubs_app_manager.h"
#include "stubs_bt_lock.h"
#include "stubs_hexdump.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_serial.h"
#include "stubs_tick.h"
void analytics_event_ams(uint8_t type, int32_t aux_info) {
}
struct {
ResponseTimeState state;
uint16_t max_period_secs;
} s_conn_mgr_states[NumBtConsumer];
void conn_mgr_set_ble_conn_response_time(GAPLEConnection *hdl, BtConsumer consumer,
ResponseTimeState state, uint16_t max_period_secs) {
s_conn_mgr_states[consumer].state = state;
s_conn_mgr_states[consumer].max_period_secs = max_period_secs;
}
GAPLEConnection *gap_le_connection_by_device(const BTDeviceInternal *device) {
return NULL;
}
BTDeviceInternal gatt_client_characteristic_get_device(BLECharacteristic characteristic_ref) {
return (BTDeviceInternal) {
.address.octets = {
0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
},
};
}
static void (*s_launcher_task_callback)(void *data);
static void *s_launcher_task_callback_data;
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
// Simple fake, can only handle one call
cl_assert_equal_p(s_launcher_task_callback, NULL);
s_launcher_task_callback = callback;
s_launcher_task_callback_data = data;
}
// Tests: Disover AMS
///////////////////////////////////////////////////////////
#define NUM_AMS_INSTANCES 2
static BLECharacteristic s_characteristics[NUM_AMS_INSTANCES][NumAMSCharacteristic] = {
// AMS instance one:
[0] = {
[AMSCharacteristicRemoteCommand] = 1,
[AMSCharacteristicEntityUpdate] = 2,
[AMSCharacteristicEntityAttribute] = 3,
},
// AMS instance two:
[1] = {
[AMSCharacteristicRemoteCommand] = 4,
[AMSCharacteristicEntityUpdate] = 5,
[AMSCharacteristicEntityAttribute] = 6,
},
};
static const BLECharacteristic s_unknown_characteristic = 999;
static void prv_assert_can_handle_characteristics(uint32_t instance_idx, bool expect_can_handle) {
for (AMSCharacteristic c = 0; c < NumAMSCharacteristic; ++c) {
cl_assert_equal_b(ams_can_handle_characteristic(s_characteristics[instance_idx][c]),
expect_can_handle);
}
}
static void prv_discover_ams(uint8_t num_instances) {
cl_assert(num_instances <= 2);
if (num_instances) {
for (int i = 0; i < num_instances && i < NUM_AMS_INSTANCES; i++) {
ams_handle_service_discovered(&s_characteristics[i][0]);
}
} else {
ams_invalidate_all_references();
}
}
void test_ams__cannot_handle_any_characteristic_after_destroy(void) {
prv_discover_ams(1);
ams_destroy();
prv_assert_can_handle_characteristics(0, false /* expect_can_handle */);
}
void test_ams__cannot_handle_unknown_characteristic(void) {
prv_discover_ams(1);
cl_assert_equal_b(ams_can_handle_characteristic(s_unknown_characteristic), false);
}
void test_ams__discover_of_ams_should_subscribe_to_entity_update_characteristic(void) {
// Pass in 2 instances, it should be able to cope with this
prv_discover_ams(2 /* num_instances */);
// Assert ams.c can now handle the characteristic reference for the first instance:
prv_assert_can_handle_characteristics(0, true /* expect_can_handle */);
// Assert ams.c can not handle the characteristic reference for the second instance:
prv_assert_can_handle_characteristics(1, false /* expect_can_handle */);
// The first instance is expected to be used.
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
fake_gatt_client_subscriptions_assert_subscribe(entity_update,
BLESubscriptionNotifications, GAPLEClientKernel);
}
void test_ams__connect_to_music_service_upon_subscribing_entity_update_characteristic(void) {
prv_discover_ams(1 /* num_instances */);
// Not connected yet (still need to subscribe):
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
// Simulate successful subscription:
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
// Music service should be connected now:
cl_assert_equal_s(music_get_connected_server_debug_name(), ams_music_server_debug_name());
// Rediscovery will disconnect music service (until resubscribed):
ams_invalidate_all_references();
prv_discover_ams(1 /* num_instances */);
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
}
void test_ams__dont_connect_music_service_if_subscribe_entity_update_characteristic_fails(void) {
prv_discover_ams(1 /* num_instances */);
// Simulate failed subscription:
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorUnlikelyError);
// Not connected because subscription failed:
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
}
void test_ams__update_characteristics_ams_not_found(void) {
// AMS Found:
prv_discover_ams(1 /* num_instances */);
// AMS Disappeared:
prv_discover_ams(0 /* num_instances */);
// Verify ams.c cannot handle the previous characteristics any more:
prv_assert_can_handle_characteristics(0, false /* expect_can_handle */);
}
// Tests: Register for Entity Updates
///////////////////////////////////////////////////////////
static const uint8_t s_register_player_entity[] = {0x00,
// Apple bug #21283910
// See ams.c, prv_get_registration_cmd_for_entity
// 0x00,
0x01, 0x02};
static const uint8_t s_register_queue_entity[] = {0x01, 0x00, 0x01, 0x02, 0x03};
static const uint8_t s_register_track_entity[] = {0x02, 0x00, 0x01, 0x02, 0x03};
void test_ams__register_for_entity_updates(void) {
prv_discover_ams(1 /* num_instances */);
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);
// Simulate successful subscription:
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
// Expect to have written the command to register for the Player entity:
fake_gatt_client_op_assert_write(entity_update, s_register_player_entity,
sizeof(s_register_player_entity), GAPLEClientKernel,
true /* is_response_required */);
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);
// Expect to have written the command to register for the Queue entity:
fake_gatt_client_op_assert_write(entity_update, s_register_queue_entity,
sizeof(s_register_queue_entity), GAPLEClientKernel,
true /* is_response_required */);
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);
// Expect to have written the command to register for the Track entity:
fake_gatt_client_op_assert_write(entity_update, s_register_track_entity,
sizeof(s_register_track_entity), GAPLEClientKernel,
true /* is_response_required */);
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), true);
// After GATT re-discovery, the registration needs to happen again
ams_invalidate_all_references();
prv_discover_ams(1 /* num_instances */);
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), false);
}
void test_ams__register_for_entity_updates_retry_if_out_of_resources(void) {
prv_discover_ams(1 /* num_instances */);
// Simulate not having enough resources to process the request:
fake_gatt_client_op_set_write_return_value(BTErrnoNotEnoughResources);
// Simulate successful subscription:
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
// Nothing should have been written because out of resources:
fake_gatt_client_op_assert_no_write();
// Resources come available again:
fake_gatt_client_op_set_write_return_value(BTErrnoOK);
// Simulate processing the callback to retry:
cl_assert(s_launcher_task_callback != NULL);
s_launcher_task_callback(s_launcher_task_callback_data);
// Expect to have written the command to register for the Player entity:
fake_gatt_client_op_assert_write(entity_update, s_register_player_entity,
sizeof(s_register_player_entity), GAPLEClientKernel,
true /* is_response_required */);
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
}
static const MusicServerImplementation s_dummy_server_implementation = {};
static void prv_set_dummy_server_connected(bool connected) {
music_set_connected_server(&s_dummy_server_implementation,
connected /* connected */);
}
void test_ams__dont_register_if_another_music_server_is_already_connected(void) {
prv_discover_ams(1 /* num_instances */);
// Not connected yet (still need to subscribe):
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
prv_set_dummy_server_connected(true /* connected */);
// Simulate successful subscription:
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
// Nothing should have been written, because there's already a music server connected.
fake_gatt_client_op_assert_no_write();
// Clean up after ourselves:
prv_set_dummy_server_connected(false /* connected */);
}
// Tests: Sending Remote Commands
///////////////////////////////////////////////////////////
static void prv_connect_ams(void) {
prv_discover_ams(1 /* num_instances */);
// Simulate successful subscription:
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
// Simulate successful write responses (for the Entity Update registration write requests):
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
ams_handle_write_response(entity_update, BLEGATTErrorSuccess);
cl_assert_equal_b(ams_is_registered_for_all_entity_updates(), true);
fake_gatt_client_op_clear_write_list();
}
static uint8_t prv_ams_command_for_music_command(MusicCommand command) {
switch (command) {
case MusicCommandPlay:
return 0x00;
case MusicCommandPause:
return 0x01;
case MusicCommandTogglePlayPause:
return 0x02;
case MusicCommandNextTrack:
return 0x03;
case MusicCommandPreviousTrack:
return 0x04;
case MusicCommandVolumeUp:
return 0x05;
case MusicCommandVolumeDown:
return 0x06;
case MusicCommandAdvanceRepeatMode:
return 0x07;
case MusicCommandAdvanceShuffleMode:
return 0x08;
case MusicCommandSkipForward:
return 0x09;
case MusicCommandSkipBackward:
return 0x0A;
case MusicCommandLike:
return 0x0B;
case MusicCommandDislike:
return 0x0C;
case MusicCommandBookmark:
return 0x0D;
default:
cl_assert(false);
return 0xFF;
}
}
void test_ams__send_remote_command(void) {
prv_connect_ams();
// Exercise all MusicCommand types (currently they are all supported):
for (MusicCommand music_cmd = 0; music_cmd < NumMusicCommand; ++music_cmd) {
music_command_send(music_cmd);
BLECharacteristic remote_command = s_characteristics[0][AMSCharacteristicRemoteCommand];
const uint8_t ams_cmd = prv_ams_command_for_music_command(music_cmd);
fake_gatt_client_op_assert_write(remote_command, &ams_cmd, sizeof(ams_cmd),
GAPLEClientKernel, true /* is_response_required */);
// Simulate receiving the response:
ams_handle_write_response(remote_command, BLEGATTErrorSuccess);
}
// Invalid/Unsupported command:
music_command_send(NumMusicCommand);
fake_gatt_client_op_assert_no_write();
}
void test_ams__send_remote_command_non_kernel_main_task(void) {
prv_connect_ams();
// Simulate Music app calling music_command_send():
stub_pebble_tasks_set_current(PebbleTask_App);
music_command_send(MusicCommandPlay);
// Process the KernelMain callback:
cl_assert(s_launcher_task_callback != NULL);
stub_pebble_tasks_set_current(PebbleTask_KernelMain);
s_launcher_task_callback(s_launcher_task_callback_data);
BLECharacteristic remote_command = s_characteristics[0][AMSCharacteristicRemoteCommand];
const uint8_t ams_cmd = prv_ams_command_for_music_command(MusicCommandPlay);
fake_gatt_client_op_assert_write(remote_command, &ams_cmd, sizeof(ams_cmd),
GAPLEClientKernel, true /* is_response_required */);
}
void test_ams__send_remote_command_non_kernel_main_task_then_disconnect(void) {
prv_connect_ams();
// Simulate Music app calling music_command_send():
stub_pebble_tasks_set_current(PebbleTask_App);
music_command_send(MusicCommandPlay);
// Simulate disconnecting:
ams_destroy();
// Process the KernelMain callback:
cl_assert(s_launcher_task_callback != NULL);
stub_pebble_tasks_set_current(PebbleTask_KernelMain);
s_launcher_task_callback(s_launcher_task_callback_data);
// No crashes, no writes
fake_gatt_client_op_assert_no_write();
}
// Tests: music_needs_user_to_start_playback_on_phone
///////////////////////////////////////////////////////////
void test_ams__music_needs_user_to_start_playback_on_phone(void) {
prv_connect_ams();
// No metadata received yet, user needs to start playback on phone
cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true);
// Received metadata
// cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), false);
// Metadata cleared
cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true);
}
// Tests: music_request_..._latency
///////////////////////////////////////////////////////////
void test_ams__music_request_reduced_latency(void) {
prv_connect_ams();
music_request_reduced_latency(true /* reduced_latency */);
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].state,
ResponseTimeMiddle);
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].max_period_secs,
MAX_PERIOD_RUN_FOREVER);
music_request_reduced_latency(false /* reduced_latency */);
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceIndefinite].state,
ResponseTimeMax);
}
void test_ams__music_request_low_latency_for_period(void) {
prv_connect_ams();
const uint32_t period_s = 1234;
music_request_low_latency_for_period(period_s * 1000);
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceMomentary].state,
ResponseTimeMin);
cl_assert_equal_i(s_conn_mgr_states[BtConsumerMusicServiceMomentary].max_period_secs,
period_s);
}
// Tests: Receiving Player updates (the happy paths)
///////////////////////////////////////////////////////////
static void prv_receive_entity_update(const uint8_t *update, uint16_t update_length) {
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_read_or_notification(entity_update, update, update_length, BLEGATTErrorSuccess);
}
void test_ams__receive_player_name_update(void) {
prv_connect_ams();
// 0000 00 00 00 4d 75 73 69 63 ...Music
uint8_t player_name_update[] = {
0x00, 0x00, 0x00, 0x4d, 0x75, 0x73, 0x69, 0x63,
};
prv_receive_entity_update(player_name_update, sizeof(player_name_update));
char player_name[MUSIC_BUFFER_LENGTH];
cl_assert_equal_b(music_get_player_name(player_name), true);
cl_assert_equal_s(player_name, "Music");
}
void test_ams__receive_player_playback_info_update(void) {
prv_connect_ams();
// Receive: playing, 200% playback rate, elapsed time 184.755s
// 0000 00 01 00 31 2c 32 2e 30 2c 31 38 34 2e 37 35 35 ...1,2.0 ,184.755
uint8_t playback_info_update[] = {
0x00, 0x01, 0x00, 0x31, 0x2c, 0x32, 0x2e, 0x30,
0x2c, 0x31, 0x38, 0x34, 0x2e, 0x37, 0x35, 0x35,
};
prv_receive_entity_update(playback_info_update, sizeof(playback_info_update));
cl_assert_equal_i(music_get_playback_state(), MusicPlayStatePlaying);
// music_get_pos relies on having a sensible track duration, so simulate receiving this too:
// 02 03 00 33 31 39 2e 35 30 37 ...319.5 07
uint8_t track_duration_update[] = {
0x02, 0x03, 0x00, 0x33, 0x31, 0x39, 0x2e, 0x35,
0x30, 0x37,
};
prv_receive_entity_update(track_duration_update, sizeof(track_duration_update));
uint32_t track_pos_ms;
uint32_t track_duration_ms;
music_get_pos(&track_pos_ms, &track_duration_ms);
cl_assert_equal_i(track_pos_ms, 184755);
cl_assert_equal_i(track_duration_ms, 319507);
cl_assert_equal_i(music_get_playback_rate_percent(), 200);
}
void test_ams__receive_player_playback_info_update_nulls(void) {
prv_connect_ams();
// Receive: paused, empty string, empty string
// 0000 00 01 00 30 2c 2c ...0,,
uint8_t playback_info_update[] = {
0x00, 0x01, 0x00, 0x30, 0x2c, 0x2c,
};
prv_receive_entity_update(playback_info_update, sizeof(playback_info_update));
cl_assert_equal_i(music_get_playback_state(), MusicPlayStatePaused);
uint32_t track_pos_ms;
uint32_t track_duration_ms;
music_get_pos(&track_pos_ms, &track_duration_ms);
cl_assert_equal_i(track_pos_ms, 0);
cl_assert_equal_i(track_duration_ms, 0);
cl_assert_equal_i(music_get_playback_rate_percent(), 0);
}
void test_ams__receive_player_volume_update(void) {
prv_connect_ams();
// Receive volume of 0.604925
// 00 02 00 30 2e 36 30 34 39 32 35 ...0.604 925
uint8_t volume_update[] = {
0x00, 0x02, 0x00, 0x30, 0x2e, 0x36, 0x30, 0x34,
0x39, 0x32, 0x35,
};
prv_receive_entity_update(volume_update, sizeof(volume_update));
cl_assert_equal_i(music_get_volume_percent(), 60);
}
// Tests: Receiving Player updates (the unhappy paths)
///////////////////////////////////////////////////////////
static void prv_assert_initial_playback_state(void) {
cl_assert_equal_i(music_get_playback_state(), MusicPlayStateUnknown);
uint32_t track_pos_ms;
uint32_t track_duration_ms;
music_get_pos(&track_pos_ms, &track_duration_ms);
cl_assert_equal_i(track_pos_ms, 0);
cl_assert_equal_i(track_duration_ms, 0);
cl_assert_equal_i(music_get_playback_rate_percent(), 0);
}
void test_ams__receive_non_numeric_player_playback_info_update(void) {
prv_connect_ams();
// Receive: 'A', 'B.0' playback rate, elapsed time 184.755s
// 0000 00 01 00 41 2c 42 2e 30 2c 31 38 34 2e 37 35 35 ...A,B.0 ,184.755
uint8_t nan_playback_info_update[] = {
0x00, 0x01, 0x00, 0x41, 0x2c, 0x42, 0x2e, 0x30,
0x2c, 0x31, 0x38, 0x34, 0x2e, 0x37, 0x35, 0x35,
};
prv_receive_entity_update(nan_playback_info_update, sizeof(nan_playback_info_update));
prv_assert_initial_playback_state();
}
void test_ams__receive_incomplete_csv_list_player_playback_info_update(void) {
prv_connect_ams();
// Receive: playing, 200% playback rate
// 0000 00 01 00 31 2c 32 2e 30 ...1,2.0
uint8_t incomplete_playback_info_update[] = {
0x00, 0x01, 0x00, 0x31, 0x2c, 0x32, 0x2e, 0x30,
};
prv_receive_entity_update(incomplete_playback_info_update,
sizeof(incomplete_playback_info_update));
prv_assert_initial_playback_state();
}
void test_ams__receive_malformed_player_volume_update(void) {
prv_connect_ams();
cl_assert_equal_i(music_get_volume_percent(), 0);
// Receive volume of 0.604925
// 00 02 00 30 2e 41 30 34 39 32 35 ...0.A04 925
uint8_t volume_update[] = {
0x00, 0x02, 0x00, 0x30, 0x2e, 0x41, 0x30, 0x34,
0x39, 0x32, 0x35,
};
prv_receive_entity_update(volume_update, sizeof(volume_update));
cl_assert_equal_i(music_get_volume_percent(), 0);
}
// Tests: Receiving Track updates
///////////////////////////////////////////////////////////
void test_ams__receive_track_artist_update(void) {
prv_connect_ams();
// 0000 02 00 00 4d 69 6c 65 73 20 44 61 76 69 73 ...Miles Davis
uint8_t track_artist_update[] = {
0x02, 0x00, 0x00, 0x4d, 0x69, 0x6c, 0x65, 0x73,
0x20, 0x44, 0x61, 0x76, 0x69, 0x73,
};
prv_receive_entity_update(track_artist_update, sizeof(track_artist_update));
char artist[MUSIC_BUFFER_LENGTH];
music_get_now_playing(NULL, artist, NULL);
cl_assert_equal_s(artist, "Miles Davis");
}
void test_ams__receive_track_title_update(void) {
prv_connect_ams();
// 0000 02 02 00 53 6f 20 57 68 61 74 ...So Wh at
uint8_t track_title_update[] = {
0x02, 0x02, 0x00, 0x53, 0x6f, 0x20, 0x57, 0x68,
0x61, 0x74,
};
prv_receive_entity_update(track_title_update, sizeof(track_title_update));
char title[MUSIC_BUFFER_LENGTH];
music_get_now_playing(title, NULL, NULL);
cl_assert_equal_s(title, "So What");
}
void test_ams__receive_track_album_update(void) {
prv_connect_ams();
// 0000 02 01 00 4b 69 6e 64 20 4f 66 20 42 6c 75 65 20 ...Kind Of Blue
// 0010 28 4c 65 67 61 63 79 20 45 64 69 74 69 6f 6e 29 (Legacy Edition)
uint8_t track_album_update[] = {
0x02, 0x01, 0x00, 0x4b, 0x69, 0x6e, 0x64, 0x20,
0x4f, 0x66, 0x20, 0x42, 0x6c, 0x75, 0x65, 0x20,
0x28, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x20,
0x45, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x29,
};
prv_receive_entity_update(track_album_update, sizeof(track_album_update));
char album[MUSIC_BUFFER_LENGTH];
music_get_now_playing(NULL, NULL, album);
cl_assert_equal_s(album, "Kind Of Blue (Legacy Edition)");
}
// Tests: Music service capabilities
///////////////////////////////////////////////////////////
void test_ams__supported_capabilities(void) {
prv_connect_ams();
// music_is_progress_reporting_supported() relies on a valid track duration
uint8_t track_duration_update[] = {
0x02, 0x03, 0x00, 0x33, 0x31, 0x39, 0x2e, 0x35,
0x30, 0x37,
};
prv_receive_entity_update(track_duration_update, sizeof(track_duration_update));
cl_assert_equal_b(music_is_playback_state_reporting_supported(), true);
cl_assert_equal_b(music_is_progress_reporting_supported(), true);
cl_assert_equal_b(music_is_volume_reporting_supported(), true);
cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), true);
for (MusicCommand cmd = 0; cmd < NumMusicCommand; ++cmd) {
cl_assert_equal_b(music_is_command_supported(cmd), true);
}
cl_assert_equal_b(music_is_command_supported(NumMusicCommand), false);
}
// Tests: Create & Destroy
///////////////////////////////////////////////////////////
void test_ams__create_again_trips_assert(void) {
cl_assert_passert(ams_create());
}
void test_ams__create_works_again_after_destroy(void) {
ams_destroy();
ams_create();
// No assert hit.
}
void test_ams__destroy_after_destroy_is_fine(void) {
ams_destroy();
ams_destroy();
// No assert hit.
}
void test_ams__destroy_disconnects_from_music_service(void) {
prv_discover_ams(1 /* num_instances */);
// Simulate successful subscription:
BLECharacteristic entity_update = s_characteristics[0][AMSCharacteristicEntityUpdate];
ams_handle_subscribe(entity_update, BLESubscriptionNotifications, BLEGATTErrorSuccess);
ams_destroy();
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
}
// Test setup
///////////////////////////////////////////////////////////
void test_ams__initialize(void) {
s_launcher_task_callback = NULL;
memset(s_conn_mgr_states, 0, sizeof(s_conn_mgr_states));
fake_rtc_init(1234, 5678);
fake_event_init();
fake_gatt_client_subscriptions_init();
fake_gatt_client_op_init();
stub_pebble_tasks_set_current(PebbleTask_KernelMain);
ams_create();
cl_assert_equal_s(music_get_connected_server_debug_name(), NULL);
}
void test_ams__cleanup(void) {
ams_destroy();
fake_gatt_client_op_deinit();
fake_gatt_client_subscriptions_deinit();
}
+263
View File
@@ -0,0 +1,263 @@
/*
* 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 "comm/ble/kernel_le_client/ams/ams_util.h"
#include "clar.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_logging.h"
// Helpers
///////////////////////////////////////////////////////////
static char s_results[10][32];
static uint32_t s_results_lengths[10];
static uint8_t s_results_count;
static uint32_t s_max_results_count;
static bool prv_result_callback(const char *value, uint32_t value_length,
uint32_t index, void *context) {
strcpy(s_results[s_results_count], value);
s_results_lengths[s_results_count] = value_length;
cl_assert_equal_i(index, s_results_count);
++s_results_count;
return (s_max_results_count > s_results_count);
}
// Tests
///////////////////////////////////////////////////////////
void test_ams_util__initialize(void) {
memset(s_results, 0, sizeof(s_results));
s_results_count = 0;
s_max_results_count = ~0;
}
void test_ams_util__cleanup(void) {
}
// ams_util_float_string_parse() tests
///////////////////////////////////////////////////////////
#define assert_float_parse(in, mul, succeeds, expected_result) \
{ \
const char *input = (const char *)in; \
const uint32_t input_length = input ? sizeof(in) : 0; \
const uint32_t multiplier = mul; \
int32_t result = 0; \
cl_assert_equal_b(succeeds, \
ams_util_float_string_parse((const char *)in, input_length, multiplier, &result)); \
if (succeeds) { \
cl_assert_equal_i(result, expected_result); \
} \
}
void test_ams_util__float_string_parse_negative_number(void) {
// "-1" * 3
assert_float_parse("-1", 3, true, -1 * 3);
}
void test_ams_util__float_string_parse_only_minus_sign(void) {
// "-" * 3
assert_float_parse("-", 3, false, 0);
}
void test_ams_util__float_string_parse_negative_number_nothing_before_separator(void) {
// "-.1" * 30
assert_float_parse("-.1", 30, true, -3);
}
void test_ams_util__float_string_parse_multiple_minusses(void) {
// "--.1" * 30
assert_float_parse("--.1", 30, false, 0);
}
void test_ams_util__float_string_parse_null(void) {
// NULL * 3
assert_float_parse(NULL, 3, false, 0);
}
void test_ams_util__float_string_parse_not_zero_terminated(void) {
uint8_t buffer[] = {'1'};
assert_float_parse(buffer, 3, true, 3);
}
void test_ams_util__float_string_parse_null_in_the_middle(void) {
uint8_t buffer[] = {'1', 0, '2'};
assert_float_parse(buffer, 3, true, 3);
}
void test_ams_util__float_string_parse_empty_string(void) {
// "" * 3
assert_float_parse("", 3, false, 0);
}
void test_ams_util__float_string_parse_not_a_number(void) {
// "hello" * 3
assert_float_parse("hello", 3, false, 0);
// " " * 3
assert_float_parse(" ", 3, false, 0);
}
void test_ams_util__float_string_parse_no_fraction(void) {
// "42" * 3
assert_float_parse("42", 3, true, 42 * 3);
}
void test_ams_util__float_string_parse_separator_but_no_fraction(void) {
// "21." * 3
assert_float_parse("21.", 3, true, 21 * 3);
}
void test_ams_util__float_string_parse_comma_decimal_separator(void) {
// "1.23456" * 3 is approx 3.7036800000000003, round up to 4
assert_float_parse("1,23456", 3, true, 4);
}
void test_ams_util__float_string_parse_period_decimal_separator(void) {
// "1.23456" * 3 is approx 3.7036800000000003, round up to 4
assert_float_parse("1.23456", 3, true, 4);
}
void test_ams_util__float_string_parse_round_down(void) {
// "0.74995" * 2 is approx 1.4999, round down to 1
assert_float_parse("0.74995", 2, true, 1);
}
void test_ams_util__float_string_parse_multiple_separators(void) {
// "1.2.2" * 3
assert_float_parse("1.2.2", 2, false, 0);
}
void test_ams_util__float_string_positive_multiplier(void) {
// "1.654321" * 33 is approx 54.592593, round up to 55
assert_float_parse("1.654321", 33, true, 55);
}
void test_ams_util__float_string_negative_multiplier(void) {
// "1.987622" * -33 is approx -65.591526, round to -66
assert_float_parse("1.987622", -33, true, -66);
}
void test_ams_util__float_string_parse_overflow_positive(void) {
// fails:
// "2147483648" * 1
assert_float_parse("2147483648", 1, false, 0);
// succeeds:
// "2147483647" * 1
assert_float_parse("2147483647", 1, true, 2147483647);
}
void test_ams_util__float_string_parse_overflow_negative(void) {
// fails:
// "-2147483649" * 1
assert_float_parse("-2147483649", 1, false, 0);
// succeeds:
// "-2147483648" * 1
assert_float_parse("-2147483648", 1, true, -2147483648);
assert_float_parse("2147483648", -1, true, -2147483648);
}
// ams_util_csv_parse() tests
///////////////////////////////////////////////////////////
#define assert_result(idx, val, len); \
cl_assert_equal_i(len, s_results_lengths[idx]); \
cl_assert_equal_i(memcmp(val, s_results[idx], len), 0);
void test_ams_util__csv_empty_string(void) {
const char empty_string[] = "";
const uint8_t count = ams_util_csv_parse(empty_string, sizeof(empty_string), NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 1);
cl_assert_equal_i(count, 1);
cl_assert_equal_s(empty_string, s_results[0]);
}
void test_ams_util__csv_empty_values(void) {
const char one_value[] = ",";
const uint8_t count = ams_util_csv_parse(one_value, sizeof(one_value), NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 2);
cl_assert_equal_i(count, 2);
assert_result(0, "", 0);
assert_result(1, "", 0);
}
void test_ams_util__csv_null(void) {
const uint8_t count = ams_util_csv_parse(NULL, 0, NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 0);
cl_assert_equal_i(count, 0);
}
void test_ams_util__csv_one_value(void) {
const char one_value[] = "A";
const uint8_t count = ams_util_csv_parse(one_value, sizeof(one_value), NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 1);
cl_assert_equal_i(count, 1);
cl_assert_equal_s(one_value, s_results[0]);
}
void test_ams_util__csv_multiple_values(void) {
const char multi_values[] = "A,B,C";
const uint8_t count = ams_util_csv_parse(multi_values, sizeof(multi_values), NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 3);
cl_assert_equal_i(count, 3);
assert_result(0, "A", 1);
assert_result(1, "B", 1);
assert_result(2, "C", 1);
}
void test_ams_util__csv_stop_after_one_value(void) {
const char multi_values[] = "A,B,C";
s_max_results_count = 1;
const uint8_t count = ams_util_csv_parse(multi_values, sizeof(multi_values), NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 1);
cl_assert_equal_i(count, 1);
assert_result(0, "A", 1);
}
void test_ams_util__csv_null_in_the_middle(void) {
const char null_middle_value[] = "A\x00 BCD,1234";
cl_assert(sizeof(null_middle_value) > 2);
const uint8_t count = ams_util_csv_parse(null_middle_value, sizeof(null_middle_value), NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 1);
cl_assert_equal_i(count, 1);
assert_result(0, "A", 1);
}
void test_ams_util__csv_buffer_not_zero_terminated(void) {
const char one_value[] = "ABCDEF";
cl_assert(sizeof(one_value) > 2);
const uint8_t count = ams_util_csv_parse(one_value,
sizeof(one_value) - 1 /* omit zero teminator */, NULL,
prv_result_callback);
cl_assert_equal_i(s_results_count, 1);
cl_assert_equal_i(count, 1);
assert_result(0, "ABCDEF", 6);
}
+644
View File
@@ -0,0 +1,644 @@
/*
* 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 "comm/ble/kernel_le_client/ancs/ancs.h"
#include "comm/ble/kernel_le_client/ancs/ancs_util.h"
#include "comm/ble/kernel_le_client/ancs/ancs_definition.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/ble/gap_le_task.h"
#include "services/common/evented_timer.h"
#include "services/common/regular_timer.h"
#include "services/normal/notifications/ancs/ancs_notifications.h"
#include "util/size.h"
#include "clar.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_analytics.h"
#include "stubs_ios_notif_pref_db.h"
#include "stubs_bt_stack.h"
#include "stubs_ble.h"
#include "stubs_pin_db.h"
#include "stubs_i18n.h"
#include "stubs_layout_layer.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pebble_tasks.h"
#include "stubs_pebble_pairing_service.h"
#include "stubs_prompt.h"
#include "stubs_timeline.h"
#include "stubs_queue.h"
#include "stubs_rand_ptr.h"
#include "stubs_reconnect.h"
#include "stubs_reminder_db.h"
#include "stubs_reminders.h"
#include "stubs_serial.h"
#include "stubs_sleep.h"
#include "stubs_system_reset.h"
#include "stubs_task_watchdog.h"
#include "stubs_nexmo.h"
#include "stubs_codepoint.h"
#include "stubs_utf8.h"
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
PebblePhoneCaller* phone_call_util_create_caller(const char *number, const char *name) {
return NULL;
}
// Fakes
///////////////////////////////////////////////////////////
#include "fake_events.h"
#include "fake_kernel_services_notifications.h"
#include "fake_new_timer.h"
#include "fake_notification_storage.h"
#include "fake_pbl_malloc.h"
#include "fake_spi_flash.h"
bool shell_prefs_get_language_english(void) {
return false;
}
static bool s_block_event_callback = false;
EventedTimerID evented_timer_register(uint32_t timeout_ms,
bool repeating,
EventedTimerCallback callback,
void* callback_data) {
if (!s_block_event_callback) {
callback(callback_data);
}
return 0;
}
// Test data
///////////////////////////////////////////////////////////
#include "ancs_test_data.h"
const uint32_t s_invalid_param_uid = 0x12;
const uint32_t s_get_wrong_data_uid = 0xee;
static BLECharacteristic s_characteristics[NumANCSCharacteristic] = { 1, 2, 3 };
// Helper Functions
///////////////////////////////////////////////////////////
static int s_num_requested_app_attributes;
static int s_num_requested_notif_attributes;
static int s_num_ds_notifications_received;
static bool s_gatt_client_op_write_should_fail_unlimited = false;
static bool s_gatt_client_op_write_should_fail_once = false;
static void prv_fake_receiving_ds_notification(size_t value_length, uint8_t *value) {
BLECharacteristic characteristic = s_characteristics[ANCSCharacteristicData];
ancs_handle_read_or_notification(characteristic, (const uint8_t *) value, value_length, 0);
}
static void prv_fake_receiving_ns_notification(size_t value_length, uint8_t *value) {
BLECharacteristic characteristic = s_characteristics[ANCSCharacteristicNotification];
ancs_handle_read_or_notification(characteristic, (const uint8_t *) value, value_length, 0);
}
static void prv_send_notification_with_event_flags(const uint8_t *ancs_notification_dict,
int event_flags) {
NSNotification ns_notification = {
.event_id = EventIDNotificationAdded,
.event_flags = event_flags,
.category_id = CategoryIDSocial,
.category_count = 1,
.uid = 1,
};
const uint32_t ancs_notification_dict_uid =
((GetNotificationAttributesMsg *)ancs_notification_dict)->notification_uid;
ns_notification.uid = ancs_notification_dict_uid;
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
}
static void prv_send_notification(const uint8_t *ancs_notification_dict) {
prv_send_notification_with_event_flags(ancs_notification_dict, 0);
}
static uint8_t *prv_serialize_timeline_item(TimelineItem *item, size_t *size_out) {
size_t payload_size = timeline_item_get_serialized_payload_size(item);
*size_out = sizeof(SerializedTimelineItemHeader) + payload_size;
uint8_t *buffer = malloc(*size_out);
timeline_item_serialize_header(item, (SerializedTimelineItemHeader *)buffer);
timeline_item_serialize_payload(item, buffer + sizeof(SerializedTimelineItemHeader),
payload_size);
return buffer;
}
void prv_cmp_last_received_notification(TimelineItem *item) {
TimelineItem *notification = fake_notification_storage_get_last_notification();
size_t size1, size2;
// Clear out the id since it is auto-generated
memset(&notification->header.id, 0, sizeof(notification->header.id));
uint8_t *buf1 = prv_serialize_timeline_item(notification, &size1);
uint8_t *buf2 = prv_serialize_timeline_item(item, &size2);
cl_assert_equal_i(size1, size2);
cl_assert_equal_m(buf1, buf2, size1);
free(buf1);
free(buf2);
}
// Called from inside prv_write_control_point_request.
// If this function is called we have requested a ds_notification
BTErrno gatt_client_op_write(BLECharacteristic characteristic,
const uint8_t *buffer,
size_t length,
GAPLEClient client) {
cl_assert_equal_i(characteristic, s_characteristics[ANCSCharacteristicControl]);
if (s_gatt_client_op_write_should_fail_once) {
s_gatt_client_op_write_should_fail_once = false;
return BTErrnoInvalidParameter;
}
if (s_gatt_client_op_write_should_fail_unlimited) {
return BTErrnoInvalidParameter;
}
const uint32_t comple_dict_uid = ((GetNotificationAttributesMsg*)s_complete_dict)->notification_uid;
const uint32_t chunked_dict_uid = ((GetNotificationAttributesMsg*)s_chunked_dict_part_one)->notification_uid;
const uint32_t message_size_attr_dict_uid = ((GetNotificationAttributesMsg*)s_message_size_attr_dict)->notification_uid;
const uint32_t invalid_dict_uid = ((GetNotificationAttributesMsg*)s_invalid_attribute_length)->notification_uid;
const uint32_t attribute_at_end_uid = ((GetNotificationAttributesMsg*)memory_with_attribute_id_at_end.attribute_data)->notification_uid;
const uint32_t loading_uid = ((GetNotificationAttributesMsg*)s_loading_response)->notification_uid;
const uint32_t no_content_uid = ((GetNotificationAttributesMsg*)s_this_message_has_no_content_response)->notification_uid;
const uint32_t multiple_complete_dict_uid = ((GetNotificationAttributesMsg*)s_multiple_complete_dicts)->notification_uid;
const uint32_t split_timestamp_uid = ((GetNotificationAttributesMsg*)s_split_timestamp_dict_part_one)->notification_uid;
const uint32_t message_dict_uid = ((GetNotificationAttributesMsg*)s_message_dict)->notification_uid;
const uint32_t app_name_title_dict_uid = ((GetNotificationAttributesMsg*)s_app_name_title_dict)->notification_uid;
const uint32_t unknown_app_message_dict_uid = ((GetNotificationAttributesMsg*)s_unknown_app_dict)->notification_uid;
const uint32_t unknown_app_unique_title_dict_uid = ((GetNotificationAttributesMsg*)s_unknown_app_unique_title_dict)->notification_uid;
const uint32_t mms_no_caption_dict_uid = ((GetNotificationAttributesMsg*)s_mms_no_caption_dict)->notification_uid;
const uint32_t mms_with_caption_dict_uid = ((GetNotificationAttributesMsg*)s_mms_with_caption_dict)->notification_uid;
const CPDSMessage *cmd_header = (const CPDSMessage *)buffer;
if (cmd_header->command_id == CommandIDGetAppAttributes) {
s_num_requested_app_attributes++;
if (strcmp((const char *)cmd_header->data, "com.tests.NotAnApp") == 0) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_fake_app_info_dict),
(uint8_t *)s_fake_app_info_dict);
} else {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_message_app_info_dict),
(uint8_t *)s_message_app_info_dict);
}
return BTErrnoOK;
}
// else: notif request
uint32_t uid = ((GetNotificationAttributesMsg *)buffer)->notification_uid;
s_num_requested_notif_attributes++;
if (uid == comple_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_complete_dict), (uint8_t*) s_complete_dict);
s_num_ds_notifications_received++;
} else if (uid == chunked_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_chunked_dict_part_one), (uint8_t*) s_chunked_dict_part_one);
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_chunked_dict_part_two), (uint8_t*) s_chunked_dict_part_two);
s_num_ds_notifications_received += 2;
} else if (uid == message_size_attr_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_message_size_attr_dict), (uint8_t*) s_message_size_attr_dict);
s_num_ds_notifications_received++;
} else if (uid == attribute_at_end_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(memory_with_attribute_id_at_end.attribute_data), (uint8_t*) memory_with_attribute_id_at_end.attribute_data);
prv_fake_receiving_ds_notification(ARRAY_LENGTH(memory_with_attribute_id_at_end_p2), (uint8_t*) memory_with_attribute_id_at_end_p2);
s_num_ds_notifications_received += 2;
} else if (uid == invalid_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_invalid_attribute_length), (uint8_t*) s_invalid_attribute_length);
s_num_ds_notifications_received++;
} else if (uid == loading_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_loading_response),
(uint8_t*) s_loading_response);
s_num_ds_notifications_received++;
} else if (uid == no_content_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_this_message_has_no_content_response),
(uint8_t*) s_this_message_has_no_content_response);
s_num_ds_notifications_received++;
} else if (uid == s_invalid_param_uid) {
ancs_handle_write_response(0, 0xA2);
s_num_ds_notifications_received++;
} else if (uid == multiple_complete_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_multiple_complete_dicts), (uint8_t*) s_multiple_complete_dicts);
s_num_ds_notifications_received += 3;
} else if (uid == split_timestamp_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_split_timestamp_dict_part_one), (uint8_t*) s_split_timestamp_dict_part_one);
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_split_timestamp_dict_part_two), (uint8_t*) s_split_timestamp_dict_part_two);
s_num_ds_notifications_received += 2;
} else if (uid == message_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_message_dict), (uint8_t*) s_message_dict);
s_num_ds_notifications_received++;
} else if (uid == app_name_title_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_app_name_title_dict), (uint8_t *)s_app_name_title_dict);
s_num_ds_notifications_received++;
} else if (uid == unknown_app_message_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_unknown_app_dict), (uint8_t *)s_unknown_app_dict);
s_num_ds_notifications_received++;
} else if (uid == unknown_app_unique_title_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_unknown_app_unique_title_dict), (uint8_t *)s_unknown_app_unique_title_dict);
s_num_ds_notifications_received++;
} else if (uid == mms_no_caption_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_mms_no_caption_dict), (uint8_t *)s_mms_no_caption_dict);
s_num_ds_notifications_received++;
} else if (uid == mms_with_caption_dict_uid) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_mms_with_caption_dict), (uint8_t *)s_mms_with_caption_dict);
s_num_ds_notifications_received++;
} else if (uid == s_get_wrong_data_uid) {
// We wanted a notification attributes message, but got a app attributes message...
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_message_app_info_dict),
(uint8_t *)s_message_app_info_dict);
s_num_ds_notifications_received++;
}
return BTErrnoOK;
}
// Tests
///////////////////////////////////////////////////////////
#define TEST_START FILESYSTEM_FILE_TEST_SPACE_BEGIN
#define TEST_SIZE (FILESYSTEM_FILE_TEST_SPACE_END - \
FILESYSTEM_FILE_TEST_SPACE_BEGIN)
void test_ancs__initialize(void) {
s_block_event_callback = false;
regular_timer_init();
s_num_requested_notif_attributes = 0;
s_num_requested_app_attributes = 0;
s_num_ds_notifications_received = 0;
s_gatt_client_op_write_should_fail_once = false;
s_gatt_client_op_write_should_fail_unlimited = false;
fake_kernel_services_notifications_reset();
fake_notification_storage_reset();
fake_event_init();
ancs_create();
ancs_handle_service_discovered(s_characteristics);
}
void test_ancs__cleanup(void) {
ancs_destroy();
cl_assert_equal_i(regular_timer_seconds_count(), 0);
cl_assert_equal_i(regular_timer_minutes_count(), 0);
regular_timer_deinit();
}
// Janky black box smoke-test to exercise the ANCS message re-assembly state
// machine
void test_ancs__should_handle_small_and_large_messages(void) {
// Get 4 complete notifications
prv_send_notification((uint8_t *)&s_complete_dict);
prv_send_notification((uint8_t *)&s_complete_dict);
prv_send_notification((uint8_t *)&s_complete_dict);
prv_send_notification((uint8_t *)&s_complete_dict);
cl_assert_equal_i(s_num_requested_notif_attributes, 4);
cl_assert_equal_i(s_num_ds_notifications_received, 4);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 4);
// Get 4 2-part notifications
prv_send_notification((uint8_t *)&s_chunked_dict_part_one);
prv_send_notification((uint8_t *)&s_chunked_dict_part_one);
prv_send_notification((uint8_t *)&s_chunked_dict_part_one);
prv_send_notification((uint8_t *)&s_chunked_dict_part_one);
cl_assert_equal_i(s_num_requested_notif_attributes, 4 + 4);
cl_assert_equal_i(s_num_ds_notifications_received, 4 + 2*4);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 4 + 4);
// Some alternating complete / 2-part notifications
prv_send_notification((uint8_t *)&s_complete_dict);
prv_send_notification((uint8_t *)&s_chunked_dict_part_one);
prv_send_notification((uint8_t *)&s_complete_dict);
prv_send_notification((uint8_t *)&s_chunked_dict_part_one);
cl_assert_equal_i(s_num_requested_notif_attributes, 8 + 4);
cl_assert_equal_i(s_num_ds_notifications_received, 12 + 1 + 2 + 1 + 2);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 4 + 4 + 4);
// Send a "corrupted" notification.
prv_send_notification((uint8_t *)&s_invalid_attribute_length);
cl_assert_equal_i(s_num_requested_notif_attributes, 12 + 1);
cl_assert_equal_i(s_num_ds_notifications_received, 18 + 1);
// No increment:
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 4 + 4 + 4);
}
void test_ancs__should_handle_message_size_attribtue(void) {
prv_send_notification((uint8_t *)&s_message_size_attr_dict);
cl_assert_equal_i(s_num_requested_notif_attributes, 1);
cl_assert_equal_i(s_num_ds_notifications_received, 1);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
}
void test_ancs__should_filter_out_loading_messages_from_mail_app(void) {
// Get notification for which we'll get a "Loading..." response:
prv_send_notification((uint8_t *)&s_loading_response);
cl_assert_equal_i(s_num_requested_notif_attributes, 1);
cl_assert_equal_i(s_num_ds_notifications_received, 1);
// Assert it got filtered out:
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
// Get notification for which we'll get a "This message has no content." response:
prv_send_notification((uint8_t *)&s_this_message_has_no_content_response);
cl_assert_equal_i(s_num_requested_notif_attributes, 2);
cl_assert_equal_i(s_num_ds_notifications_received, 2);
// Assert it got filtered out:
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
}
void test_ancs__should_filter_out_duplicate_messages(void) {
// With an empty db, new notifications should be added as usual
prv_send_notification((uint8_t *)&s_complete_dict);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
cl_assert_equal_i(fake_notification_storage_get_store_count(), 1);
cl_assert_equal_i(fake_notification_storage_get_remove_count(), 0);
// We should reject any notification that matches and has the exact same uid
uint32_t uid = ((GetNotificationAttributesMsg *)&s_complete_dict)->notification_uid;
fake_notification_storage_set_existing_ancs_notification(&(Uuid)UUID_SYSTEM, uid);
prv_send_notification((uint8_t *)&s_complete_dict);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
cl_assert_equal_i(fake_notification_storage_get_store_count(), 1);
cl_assert_equal_i(fake_notification_storage_get_remove_count(), 0);
// If there's a notification that matches with a different uid, we update the notification by
// removing and then storing again (we don't send a NotificationAdded event)
fake_notification_storage_set_existing_ancs_notification(&(Uuid)UUID_SYSTEM, UINT32_MAX);
prv_send_notification((uint8_t *)&s_complete_dict);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
cl_assert_equal_i(fake_notification_storage_get_store_count(), 2);
cl_assert_equal_i(fake_notification_storage_get_remove_count(), 1);
}
void test_ancs__should_handle_split_timestamp_messages(void) {
prv_send_notification((uint8_t *)&s_split_timestamp_dict_part_one);
cl_assert_equal_i(s_num_requested_notif_attributes, 1);
cl_assert_equal_i(s_num_ds_notifications_received, 2);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
}
void test_ancs__attribute_at_end(void) {
prv_send_notification((uint8_t *)&memory_with_attribute_id_at_end.attribute_data);
cl_assert_equal_i(s_num_requested_notif_attributes, 1 );
cl_assert_equal_i(s_num_ds_notifications_received, 2);
}
void test_ancs__app_name_cache(void) {
prv_send_notification((uint8_t *)&s_message_dict);
prv_send_notification((uint8_t *)&s_message_dict);
cl_assert_equal_i(s_num_requested_notif_attributes, 2);
// should have gotten cached the second time around
cl_assert_equal_i(s_num_requested_app_attributes, 1);
cl_assert_equal_i(s_num_ds_notifications_received, 2);
}
void test_ancs__ancs_invalid_param(void) {
NSNotification ns_notification = {
.event_id = EventIDNotificationAdded,
.event_flags = 0,
.category_id = CategoryIDSocial,
.category_count = 1,
.uid = 0,
};
const uint32_t comple_dict_uid = ((GetNotificationAttributesMsg*)s_complete_dict)->notification_uid;
ns_notification.uid = s_invalid_param_uid;
// This will return with an error ANCS_INVALID_PARAM
// Should not get re-requested
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(s_num_requested_notif_attributes, 1 );
cl_assert_equal_i(s_num_ds_notifications_received, 1);
ns_notification.uid = comple_dict_uid;
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(s_num_requested_notif_attributes, 2);
cl_assert_equal_i(s_num_ds_notifications_received, 2);
ns_notification.uid = s_invalid_param_uid;
// This will return with an error ANCS_INVALID_PARAM
// Should not get re-requested
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(s_num_requested_notif_attributes, 3);
cl_assert_equal_i(s_num_ds_notifications_received, 3);
ns_notification.uid = s_invalid_param_uid;
// This will return with an error ANCS_INVALID_PARAM
// Should not get re-requested
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(s_num_requested_notif_attributes, 4);
cl_assert_equal_i(s_num_ds_notifications_received, 4);
ns_notification.uid = comple_dict_uid;
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(s_num_requested_notif_attributes, 5);
cl_assert_equal_i(s_num_ds_notifications_received, 5);
}
extern ANCSClientState prv_get_state(void);
extern void prv_check_ancs_alive(void);
void test_ancs__alive_check_disconnection(void) {
prv_check_ancs_alive();
// check we're in the alive check state and we sent a single request
cl_assert_equal_i(prv_get_state(), ANCSClientStateAliveCheck);
cl_assert_equal_i(s_num_requested_notif_attributes, 1);
// simulate a disconnection/reconnection
ancs_handle_service_removed(s_characteristics, NumANCSCharacteristic);
ancs_handle_service_discovered(s_characteristics);
// we should be back in the Idle state
cl_assert_equal_i(prv_get_state(), ANCSClientStateIdle);
// Make sure we can still receive notifications
prv_send_notification((uint8_t *)&s_complete_dict);
cl_assert_equal_i(s_num_requested_notif_attributes, 2);
cl_assert_equal_i(s_num_ds_notifications_received, 1);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
}
void test_ancs__notification_dismissal(void) {
NSNotification ns_notification = {
.event_id = EventIDNotificationRemoved,
.event_flags = 0,
.category_id = CategoryIDSocial,
.category_count = 1,
};
// Notification removal without DIS service - notification shouldn't be acted upon
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(fake_kernel_services_notifications_acted_upon_count(), 0);
// DIS service / iOS 9+ detected - enabling notification dismissal
ancs_handle_ios9_or_newer_detected();
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(fake_kernel_services_notifications_acted_upon_count(), 1);
}
void test_ancs__notification_parsing(void) {
// Test a recognized app with a duplicated title
// Run multiple times to make sure we're not corrupting the app name cache
for (int i = 0; i < 4; ++i) {
prv_send_notification((uint8_t *)&s_app_name_title_dict);
prv_cmp_last_received_notification(&s_app_name_title_parsed_item);
}
// Test an unrecognized app with a duplicated title
prv_send_notification((uint8_t *)&s_unknown_app_dict);
prv_cmp_last_received_notification(&s_unknown_app_parsed_item);
// Make sure both apps attributes were requested (Messages and FakeApp)
cl_assert_equal_i(s_num_requested_app_attributes, 2);
// Test a recognized app with a unique title
prv_send_notification((uint8_t *)&s_message_dict);
prv_cmp_last_received_notification(&s_message_parsed_item);
// Test an unrecognized app with a unique title
prv_send_notification((uint8_t *)&s_unknown_app_unique_title_dict);
prv_cmp_last_received_notification(&s_unknown_app_unique_title_parsed_item);
// Test an MMS without a caption
prv_send_notification_with_event_flags((uint8_t *)&s_mms_no_caption_dict, EventFlagMultiMedia);
prv_cmp_last_received_notification(&s_mms_no_caption_parsed_item);
// Test an MMS with a caption
prv_send_notification_with_event_flags((uint8_t *)&s_mms_with_caption_dict, EventFlagMultiMedia);
prv_cmp_last_received_notification(&s_mms_with_caption_parsed_item);
// Test a third party notification with the MultiMedia EventFlag
prv_send_notification_with_event_flags((uint8_t *)&s_unknown_app_unique_title_dict, EventFlagMultiMedia);
prv_cmp_last_received_notification(&s_unknown_app_unique_title_parsed_item);
}
// Make sure we send an ANCS_DISCONNECTED event whenever our session goes away
void test_ancs__disconnection(void) {
// Simulate a disconnection/reconnection
ancs_handle_service_removed(s_characteristics, NumANCSCharacteristic);
ancs_handle_service_discovered(s_characteristics);
cl_assert_equal_i(fake_event_get_last().type, PEBBLE_ANCS_DISCONNECTED_EVENT);
fake_event_clear_last();
// If we unexpectedly register another session, make sure we send the event
ancs_handle_service_discovered(s_characteristics);
cl_assert_equal_i(fake_event_get_last().type, PEBBLE_ANCS_DISCONNECTED_EVENT);
fake_event_clear_last();
ancs_invalidate_all_references();
cl_assert_equal_i(fake_event_get_last().type, PEBBLE_ANCS_DISCONNECTED_EVENT);
// Make sure that losing BT altogether sends the event
ancs_destroy();
cl_assert_equal_i(fake_event_get_last().type, PEBBLE_ANCS_DISCONNECTED_EVENT);
fake_event_clear_last();
}
void test_ancs__unrequested_notifications(void) {
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_complete_dict), (uint8_t*) s_complete_dict);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
prv_fake_receiving_ds_notification(ARRAY_LENGTH(s_message_app_info_dict),
(uint8_t *)s_message_app_info_dict);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
}
void test_ancs__handle_unexpected_notifications(void) {
NSNotification ns_notification = {
.event_id = EventIDNotificationAdded,
.event_flags = 0,
.category_id = CategoryIDSocial,
.category_count = 1,
.uid = s_get_wrong_data_uid,
};
prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 0);
// And make sure we get to a state where we can handle more messages
prv_send_notification((uint8_t *)&s_complete_dict);
cl_assert_equal_i(s_num_requested_notif_attributes, 2);
cl_assert_equal_i(s_num_ds_notifications_received, 2);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
}
void test_ancs__get_notif_attributes_retry(void) {
s_gatt_client_op_write_should_fail_once = true;
prv_send_notification((uint8_t *)&s_complete_dict);
// We will be successful on the retry (second attempt)
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
s_gatt_client_op_write_should_fail_unlimited = true;
prv_send_notification((uint8_t *)&s_complete_dict);
// The retry fails and we give up on this one
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 1);
// And make sure we get to a state where we can handle more messages
s_gatt_client_op_write_should_fail_unlimited = false;
prv_send_notification((uint8_t *)&s_complete_dict);
cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 2);
}
void test_ancs__reset_after_retry(void) {
s_block_event_callback = true;
s_gatt_client_op_write_should_fail_once = true;
prv_send_notification((uint8_t *)&s_complete_dict);
ancs_invalidate_all_references();
cl_assert_equal_i(prv_get_state(), ANCSClientStateIdle);
}
// No Longer Supported
//void test_ancs__should_handle_response_with_multiple_notifications(void) {
//
// NSNotification ns_notification = {
// .event_id = EventIDNotificationAdded,
// .event_flags = 0,
// .category_id = CategoryIDSocial,
// .category_count = 1,
// .uid = 0,
// };
//
// const uint32_t multiple_complete_dict_uid = ((GetNotificationAttributesMsg*)s_multiple_complete_dicts)->notification_uid;
// ns_notification.uid = multiple_complete_dict_uid;
// prv_fake_receiving_ns_notification(sizeof(ns_notification), (uint8_t*) &ns_notification);
// cl_assert_equal_i(s_num_requested_notif_attributes, 1);
// cl_assert_equal_i(s_num_ds_notifications_received, 3);
//
// // The last one was a phone notification but I changed it so it no longer is
// cl_assert_equal_i(fake_kernel_services_notifications_ancs_notifications_count(), 3);
//}
+196
View File
@@ -0,0 +1,196 @@
/*
* 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 "clar.h"
#include "applib/persist_map.h"
#include "applib/persist_private.h"
#include "comm/ble/app_profiles/ancs_app_storage.h"
#include "drivers/crc.h"
#include "kernel/services/file.h"
#include "system/filesystem.h"
#include "system/logging.h"
#include "util/size.h"
#include <ctype.h>
#include <stdio.h>
#include <string.h>
// Stubs
////////////////////////////////////
#include "stubs_passert.h"
#include "stubs_serial.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_system_reset.h"
#include "stubs_task_watchdog.h"
// Fakes
////////////////////////////////////
#include "fake_session.h"
#include "fake_spi_flash.h"
#include "fake_system_task.h"
#include "fake_new_timer.h"
#include "fake_pbl_malloc.h"
// Tests
////////////////////////////////////
#define TEST_START FILESYSTEM_FILE_TEST_SPACE_BEGIN
#define TEST_SIZE (FILESYSTEM_FILE_TEST_SPACE_END - \
FILESYSTEM_FILE_TEST_SPACE_BEGIN)
void test_ancs_app_storage__initialize(void) {
fake_spi_flash_init(TEST_START, TEST_SIZE);
file_system_format();
file_system_reset();
ancs_app_storage_init();
}
void test_ancs_app_storage__cleanup(void) {
ancs_app_storage_deinit();
}
void test_ancs_app_storage__one_app(void) {
ANCSAppData app_data = {
.bundle_id = "com.getpebble.PebbleApp",
.display_name = "Pebble",
.flags = ANCSAppIsBlocked,
};
app_data.is_meta_changed = true;
ancs_app_storage_save(&app_data);
ANCSAppData app_data_out = { 0 };
ancs_app_storage_load(app_data.bundle_id, &app_data_out);
cl_assert_equal_s(app_data.bundle_id, app_data_out.bundle_id);
cl_assert_equal_i(app_data.flags, app_data_out.flags);
cl_assert_equal_s(app_data.display_name, app_data_out.display_name);
ancs_app_destroy_buffer(&app_data_out);
}
void test_ancs_app_storage__overwrite(void) {
ANCSAppData app_data = {
.bundle_id = "com.getpebble.PebbleApp",
.display_name = "Pebble",
.flags = ANCSAppIsBlocked,
};
app_data.is_meta_changed = true;
ancs_app_storage_save(&app_data);
app_data.display_name = "Pebble 2";
app_data.is_meta_changed = true;
ancs_app_storage_save(&app_data);
ANCSAppData app_data_out = { 0 };
ancs_app_storage_load(app_data.bundle_id, &app_data_out);
cl_assert_equal_s(app_data.bundle_id, app_data_out.bundle_id);
cl_assert_equal_i(app_data.flags, app_data_out.flags);
cl_assert_equal_s(app_data.display_name, app_data_out.display_name);
ancs_app_destroy_buffer(&app_data_out);
}
static uint32_t get_key(const char* bundle_id) {
return legacy_defective_checksum_memory(bundle_id, strlen(bundle_id));
}
void test_ancs_app_storage__hash_collisions(void) {
ANCSAppData app_data = {
.flags = ANCSAppFlagNone,
};
// Courtesy of http://programmers.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and-speed
char* collide_pairs[][2] = {
{ "codding", "gnu" },
{ "exhibiters", "schlager" },
};
for (unsigned int i = 0; i < ARRAY_LENGTH(collide_pairs); ++i) {
for (unsigned int j = 0; j < 2; ++j) {
char* name = collide_pairs[i][j];
app_data.bundle_id = name;
app_data.display_name = name;
app_data.is_meta_changed = true;
uint32_t key = get_key(name);
PBL_LOG(LOG_LEVEL_DEBUG, "name: %s, key: %u", name, (unsigned) key);
ancs_app_storage_save(&app_data);
}
}
ANCSAppData app_data_out = { 0 };
for (unsigned int i = 0; i < ARRAY_LENGTH(collide_pairs); ++i) {
for (unsigned int j = 0; j < 2; ++j) {
char* name = collide_pairs[i][j];
app_data.bundle_id = name;
app_data.display_name = name;
ancs_app_storage_load(name, &app_data_out);
cl_assert_equal_s(app_data.bundle_id, app_data_out.bundle_id);
cl_assert_equal_i(app_data.flags, app_data_out.flags);
cl_assert_equal_s(app_data.display_name, app_data_out.display_name);
ancs_app_destroy_buffer(&app_data_out);
}
}
}
void test_ancs_app_storage__iter(void) {
ANCSAppData apps[] = {
{ .bundle_id = "com.apple.MobileSMS", .display_name = "Messages" },
{ .bundle_id = "com.apple.facetime", .display_name = "FaceTime" },
{ .bundle_id = "com.facebook.Messenger", .display_name = "Facebook" },
{ .bundle_id = "com.atebits.Tweetie2", .display_name = "Twitter" },
{ .bundle_id = "com.apple.mobilecal", .display_name = "Calender" },
{ .bundle_id = "com.blackberry.bbm1", .display_name = "BBM" },
{ .bundle_id = "net.whatsapp.WhatsApp", .display_name = "WhatsApp" },
{ .bundle_id = "com.toyopagroup.picaboo", .display_name = "Snapchat" },
{ .bundle_id = "com.kik.chat", .display_name = "Kik Chat" },
{ .bundle_id = "com.apple.mobilemail", .display_name = "Mail" },
{ .bundle_id = "com.yahoo.Aerogram", .display_name = "YMail" },
{ .bundle_id = "co.inboxapp.inbox", .display_name = "Inbox" },
{ .bundle_id = "com.google.Gmail", .display_name = "Gmail" },
};
for (unsigned int i = 0; i < ARRAY_LENGTH(apps); ++i) {
apps[i].is_meta_changed = true;
apps[i].is_local_changed = true;
ancs_app_storage_save(&apps[i]);
}
ANCSAppData app_data_out = { 0 };
for (unsigned int i = 0; i < ARRAY_LENGTH(apps); ++i) {
PBL_LOG(LOG_LEVEL_DEBUG, "i: %d, name: %s", i, apps[i].bundle_id);
ancs_app_storage_load(apps[i].bundle_id, &app_data_out);
cl_assert_equal_s(apps[i].bundle_id, app_data_out.bundle_id);
cl_assert_equal_i(apps[i].flags, app_data_out.flags);
cl_assert_equal_s(apps[i].display_name, app_data_out.display_name);
ancs_app_destroy_buffer(&app_data_out);
}
ancs_app_storage_iter_begin();
for (unsigned int i = 0; ancs_app_storage_next(&app_data_out); ++i) {
PBL_LOG(LOG_LEVEL_DEBUG, "i: %d, name: %s, name_out: %s", i, apps[i].bundle_id, app_data_out.bundle_id);
cl_assert_equal_s(apps[i].bundle_id, app_data_out.bundle_id);
}
}
+181
View File
@@ -0,0 +1,181 @@
/*
* 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 "comm/ble/kernel_le_client/ancs/ancs.h"
#include "comm/ble/kernel_le_client/ancs/ancs_types.h"
#include "comm/ble/kernel_le_client/ancs/ancs_util.h"
#include "util/buffer.h"
#include "clar.h"
#include <string.h>
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_analytics.h"
#include "stubs_ble.h"
#include "stubs_bt_stack.h"
#include "stubs_logging.h"
#include "stubs_serial.h"
#include "stubs_passert.h"
#include "stubs_queue.h"
#include "stubs_mutex.h"
#include "stubs_system_reset.h"
#include "stubs_task_watchdog.h"
#include "stubs_pbl_malloc.h"
#include "stubs_reconnect.h"
// Test data
///////////////////////////////////////////////////////////
#include "ancs_test_data.h"
// Tests
///////////////////////////////////////////////////////////
void test_ancs_util__should_parse_complete_notif_attr_dict(void) {
bool error = false;
cl_assert(ancs_util_is_complete_notif_attr_response(s_complete_dict, sizeof(s_complete_dict), &error));
cl_assert(!error);
}
void test_ancs_util__should_identify_missing_attribute(void) {
bool error = false;
cl_assert(!ancs_util_is_complete_notif_attr_response(s_missing_last_attribute, sizeof(s_missing_last_attribute),
&error));
cl_assert(!error);
}
void test_ancs_util__should_identify_invalid_attr_length(void) {
bool error = false;
cl_assert(!ancs_util_is_complete_notif_attr_response(s_invalid_attribute_length, sizeof(s_invalid_attribute_length),
&error));
cl_assert(error);
}
void test_ancs_util__should_parse_incomplete_last_attribute(void) {
bool error = false;
cl_assert(!ancs_util_is_complete_notif_attr_response(s_chunked_dict_part_one, sizeof(s_chunked_dict_part_one),
&error));
cl_assert(!error);
}
void test_ancs_util__should_not_parse_malformed_notif_attr_dict(void) {
bool error = false;
cl_assert(!ancs_util_is_complete_notif_attr_response(s_chunked_dict_part_two, sizeof(s_chunked_dict_part_two),
&error));
cl_assert(error);
}
void test_ancs_util__should_extract_dict_from_buffer(void) {
static const char* expected_message = "This is a very complicated case, Maude. You know, a lotta ins, lotta outs, lotta what-have-you's. And, uh, lotta strands to keep in my head, man. Lotta strands in old Duder's head. Luckily I'm adherin";
int bytes_written;
Buffer* b = buffer_create(500);
bytes_written = buffer_add(b, s_chunked_dict_part_one, sizeof(s_chunked_dict_part_one));
cl_assert_equal_i(bytes_written, sizeof(s_chunked_dict_part_one));
bytes_written = buffer_add(b, s_chunked_dict_part_two, sizeof(s_chunked_dict_part_two));
cl_assert_equal_i(bytes_written, sizeof(s_chunked_dict_part_two));
bool error = false;
cl_assert(ancs_util_is_complete_notif_attr_response(b->data, b->bytes_written, &error));
cl_assert(!error);
ANCSAttribute* attr_ptrs[NUM_FETCHED_NOTIF_ATTRIBUTES];
// Only pass the attribute data to ancs_util_get_attr_ptrs:
const GetNotificationAttributesMsg *msg =
(const GetNotificationAttributesMsg*) b->data;
const size_t attributes_length = b->bytes_written - sizeof(*msg);
cl_assert(ancs_util_get_attr_ptrs(msg->attributes_data, attributes_length, s_fetched_notif_attributes,
NUM_FETCHED_NOTIF_ATTRIBUTES, attr_ptrs, &error));
cl_assert(!error);
ANCSAttribute* message = (ANCSAttribute*) attr_ptrs[3];
cl_assert(strncmp((char*) message->value, expected_message, message->length) == 0);
free(b);
}
void test_ancs_util__should_detect_duplicate_message(void) {
#if(0)
// Extract the dict
static const char* expected_message = "This is a very complicated case, Maude. You know, a lotta ins, lotta outs, lotta what-have-you's. And, uh, lotta strands to keep in my head, man. Lotta strands in old Duder's head. Luckily I'm adherin";
ancs_util_reset_notification_cache();
Buffer* b = buffer_create(500);
cl_assert_equal_i(buffer_add(b, s_chunked_dict_part_one, sizeof(s_chunked_dict_part_one)), sizeof(s_chunked_dict_part_one));
cl_assert_equal_i(buffer_add(b, s_chunked_dict_part_two, sizeof(s_chunked_dict_part_two)), sizeof(s_chunked_dict_part_two));
bool error = false;
cl_assert(ancs_util_is_complete_notif_attr_response(b->data, b->bytes_written, &error));
cl_assert(!error);
ANCSAttribute *attr_ptrs[NUM_FETCHED_NOTIF_ATTRIBUTES];
const GetNotificationAttributesMsg *msg =
(const GetNotificationAttributesMsg*) b->data;
const size_t attributes_length = b->bytes_written - sizeof(*msg);
cl_assert(ancs_util_get_attr_ptrs(msg->attributes_data, attributes_length, s_fetched_notif_attributes,
NUM_FETCHED_NOTIF_ATTRIBUTES, attr_ptrs, &error));
cl_assert(!error);
ANCSAttribute* message = (ANCSAttribute*) attr_ptrs[3];
cl_assert(strncmp((char*) message->value, expected_message, message->length) == 0);
// Ensure the dupe is detected
bool was_added_to_cache = ancs_util_cache_notification(
attr_ptrs[FetchedAttributeIndexAppID],
attr_ptrs[FetchedAttributeIndexTitle],
attr_ptrs[FetchedAttributeIndexSubtitle],
attr_ptrs[FetchedAttributeIndexMessage],
attr_ptrs[FetchedAttributeIndexDate]
);
cl_assert(was_added_to_cache);
was_added_to_cache = ancs_util_cache_notification(
attr_ptrs[FetchedAttributeIndexAppID],
attr_ptrs[FetchedAttributeIndexTitle],
attr_ptrs[FetchedAttributeIndexSubtitle],
attr_ptrs[FetchedAttributeIndexMessage],
attr_ptrs[FetchedAttributeIndexDate]
);
cl_assert(was_added_to_cache == false);
// Ensure the dupe is no longer detected
ancs_util_reset_notification_cache();
was_added_to_cache = ancs_util_cache_notification(
attr_ptrs[FetchedAttributeIndexAppID],
attr_ptrs[FetchedAttributeIndexTitle],
attr_ptrs[FetchedAttributeIndexSubtitle],
attr_ptrs[FetchedAttributeIndexMessage],
attr_ptrs[FetchedAttributeIndexDate]
);
cl_assert(was_added_to_cache);
free(b);
#endif
}
+216
View File
@@ -0,0 +1,216 @@
/*
* 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 "clar.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/bt_conn_mgr.h"
#include "comm/bt_conn_mgr_impl.h"
#include "services/common/regular_timer.h"
// Fakes
#include "fake_gap_le_connect_params.h"
#include "fake_new_timer.h"
#include "fake_rtc.h"
#include "fake_system_task.h"
#include "fake_pbl_malloc.h"
// Stubs
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_lock.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
extern void conn_mgr_handle_desired_state_granted(GAPLEConnection *hdl,
ResponseTimeState granted_state);
// Stubs
/////
bool gap_le_connection_is_valid(const GAPLEConnection *conn) {
return (conn != NULL);
}
GAPLEConnection *gap_le_connection_any(void) {
return NULL;
}
void prv_regular_timer_spend_seconds(uint32_t seconds) {
for (uint32_t i = 0; i < seconds; ++i) {
fake_rtc_increment_ticks(RTC_TICKS_HZ);
regular_timer_fire_seconds(1);
// bt_conn_mgr offloads the callback to KernelBG
fake_system_task_callbacks_invoke_pending();
}
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
// Tests
///////////////////////////////////////////////////////////
static int s_granted_count;
static GAPLEConnection s_hdl;
void test_bt_conn_mgr__initialize(void) {
s_granted_count = 0;
fake_rtc_init(0, 0);
regular_timer_init();
fake_gap_le_connect_params_init();
s_hdl.conn_mgr_info = bt_conn_mgr_info_init();
}
void test_bt_conn_mgr__cleanup(void) {
regular_timer_deinit();
}
void test_bt_conn_mgr__ble_latency_mgr(void) {
// 1 consumer at fastest rate should result in fastest rate getting scheduled
conn_mgr_set_ble_conn_response_time(
&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMin, 100);
uint16_t secs_to_wait;
ResponseTimeState state;
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMin);
cl_assert_equal_i(secs_to_wait, 100);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeMin);
// another consumer at lower rate should not have any effect
fake_gap_le_connect_params_reset_last_requested();
conn_mgr_set_ble_conn_response_time(
&s_hdl, BtConsumerUnitTests, ResponseTimeMiddle, 30);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMin);
cl_assert_equal_i(secs_to_wait, 100);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeInvalid);
// removing the fastest consumer should result in the next fastest being scheduled, but only
// after an "inactivity timeout":
conn_mgr_set_ble_conn_response_time(
&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMax, 0);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMin);
cl_assert_equal_i(secs_to_wait, BT_CONN_MGR_INACTIVITY_TIMEOUT_SECS);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeInvalid);
prv_regular_timer_spend_seconds(BT_CONN_MGR_INACTIVITY_TIMEOUT_SECS);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMiddle);
cl_assert_equal_i(secs_to_wait, 30 - BT_CONN_MGR_INACTIVITY_TIMEOUT_SECS);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeMiddle);
// removing all consumers we should fall back to slowest interval, but only
// after an "inactivity timeout":
fake_gap_le_connect_params_reset_last_requested();
conn_mgr_set_ble_conn_response_time(
&s_hdl, BtConsumerUnitTests, ResponseTimeMax, 0);
prv_regular_timer_spend_seconds(BT_CONN_MGR_INACTIVITY_TIMEOUT_SECS);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMax);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeMax);
// if nothing else is scheduled, middle rate should get picked up right away
conn_mgr_set_ble_conn_response_time(
&s_hdl, BtConsumerUnitTests, ResponseTimeMiddle, 30);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMiddle);
cl_assert_equal_i(secs_to_wait, 30);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeMiddle);
// higher rate should take over lower rate already scheduled
conn_mgr_set_ble_conn_response_time(
&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMin, 25);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMin);
cl_assert_equal_i(secs_to_wait, 25);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeMin);
// two requests at same high rate, longest time should be selected as timeout
fake_gap_le_connect_params_reset_last_requested();
conn_mgr_set_ble_conn_response_time(
&s_hdl, BtConsumerUnitTests, ResponseTimeMin, 250);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMin);
cl_assert_equal_i(secs_to_wait, 250);
cl_assert_equal_i(fake_gap_le_connect_params_get_last_requested(), ResponseTimeInvalid);
bt_conn_mgr_info_deinit(&s_hdl.conn_mgr_info);
}
static void prv_granted_handler(void) {
++s_granted_count;
}
void test_bt_conn_mgr__granted_handler_request_max_no_existing_node(void) {
fake_gap_le_connect_params_set_actual_state(ResponseTimeMax);
conn_mgr_set_ble_conn_response_time_ext(&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMax, 1,
prv_granted_handler);
// Expect granted handler to be called immediately:
cl_assert_equal_i(s_granted_count, 1);
}
void test_bt_conn_mgr__granted_handler_request_existing(void) {
fake_gap_le_connect_params_set_actual_state(ResponseTimeMax);
conn_mgr_set_ble_conn_response_time_ext(&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMin, 1,
prv_granted_handler);
cl_assert_equal_i(s_granted_count, 0);
// Simulate that the requested state takes effect:
fake_gap_le_connect_params_set_actual_state(ResponseTimeMin);
conn_mgr_handle_desired_state_granted(&s_hdl, ResponseTimeMin);
cl_assert_equal_i(s_granted_count, 1);
conn_mgr_set_ble_conn_response_time_ext(&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMin, 1,
prv_granted_handler);
cl_assert_equal_i(s_granted_count, 2);
conn_mgr_set_ble_conn_response_time_ext(&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMiddle,
1, prv_granted_handler);
cl_assert_equal_i(s_granted_count, 3);
conn_mgr_set_ble_conn_response_time_ext(&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMax,
1, prv_granted_handler);
cl_assert_equal_i(s_granted_count, 4);
}
void test_bt_conn_mgr__request_max_time_while_no_requests_are_running(void) {
uint16_t secs_to_wait;
ResponseTimeState state;
// Always start off with ResponseTimeMax:
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMax);
// Requesting ResponseTimeMax should have no effect:
conn_mgr_set_ble_conn_response_time(&s_hdl, BtConsumerLeServiceDiscovery, ResponseTimeMax, 1);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMax);
// Not even after waiting 10 seconds:
prv_regular_timer_spend_seconds(10);
state = conn_mgr_get_latency_for_le_connection(&s_hdl, &secs_to_wait);
cl_assert_equal_i(state, ResponseTimeMax);
bt_conn_mgr_info_deinit(&s_hdl.conn_mgr_info);
}
+955
View File
@@ -0,0 +1,955 @@
/*
* 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 "comm/ble/gap_le_advert.h"
#include "comm/ble/gap_le_connection.h"
#include "services/common/regular_timer.h"
#include "util/size.h"
#include "clar.h"
// Fakes
///////////////////////////////////////////////////////////
#include "fake_GAPAPI.h"
#include "fake_new_timer.h"
#include "fake_rtc.h"
#include "fake_system_task.h"
#include "fake_pbl_malloc.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_analytics.h"
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_lock.h"
#include "stubs_gatt_client_discovery.h"
#include "stubs_gatt_client_subscriptions.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_prompt.h"
void gap_le_connect_bluetopia_connection_callback(uint32_t stack_id,
GAP_LE_Event_Data_t* event_data,
uint32_t CallbackParameter) {
}
bool static s_is_connected_as_slave = false;
bool gap_le_connect_is_connected_as_slave(void) {
return s_is_connected_as_slave;
}
void ble_legacy_discovery_enable(uint32_t timeout_secs) {
}
void gap_le_slave_reconnect_stop(void) {
}
void gap_le_slave_reconnect_start(void) {
}
void gatt_service_changed_server_cleanup_by_connection(GAPLEConnection *connection) {
}
int HCI_LE_Set_Advertise_Enable(unsigned int BluetoothStackID, ...) {
return (0);
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
static uint32_t s_unscheduled_cb_count;
static void * s_unscheduled_cb_data = "Callback Data";
static GAPLEAdvertisingJobRef s_unscheduled_job;
static bool s_unscheduled_completed;
static void unscheduled_callback(GAPLEAdvertisingJobRef job,
bool completed,
void *cb_data) {
s_unscheduled_job = job;
++s_unscheduled_cb_count;
s_unscheduled_completed = completed;
cl_assert_equal_p(cb_data, s_unscheduled_cb_data);
}
void test_gap_le_advert__initialize(void) {
fake_GAPAPI_init();
s_unscheduled_cb_count = 0;
s_unscheduled_job = NULL;
s_unscheduled_completed = false;
// This bypasses the work-around for the CC2564 advertising bug, that pauses the round-robinning
// through scheduled advertisment jobs:
s_is_connected_as_slave = true;
regular_timer_init();
gap_le_advert_init();
}
void test_gap_le_advert__cleanup(void) {
gap_le_advert_deinit();
// Make sure deinit did disable advertising and clean up timer:
cl_assert(!gap_le_is_advertising_enabled());
cl_assert_equal_i(regular_timer_seconds_count(), 0);
regular_timer_deinit();
}
//! Helper to assert whether a piece of ad_data is set to the controller.
//! For convenience, C strings are used (while in reality it can be arbitrary
//! binary data).
#define assert_ad_data(ad_data_cstring) \
{ \
Advertising_Data_t ad_data_out; \
const size_t data_length = strlen(ad_data_cstring) + 1; \
cl_assert_equal_i(gap_le_get_advertising_data(&ad_data_out), data_length); \
cl_assert_equal_s((const char *) &ad_data_out, ad_data_cstring); \
}
//! Helper to create BLEAdData from C strings.
//! In reality, people will use ble_ad_create() and the ble_ad_set_* functions,
//! but that's part of another test.
static BLEAdData *create_ad(const char ad_data[],
const char scan_resp_data[]) {
const size_t ad_data_length = ad_data ? strlen(ad_data) + 1 : 0;
const size_t scan_resp_data_length =
scan_resp_data ? strlen(scan_resp_data) + 1 : 0;
BLEAdData *ad = (BLEAdData *) malloc(sizeof(BLEAdData) +
ad_data_length +
scan_resp_data_length);
ad->ad_data_length = ad_data_length;
ad->scan_resp_data_length = scan_resp_data_length;
if (ad_data_length) {
strncpy((char *) ad->data,
ad_data, ad_data_length);
}
if (scan_resp_data_length) {
strncpy((char *) ad->data + ad_data_length,
scan_resp_data, scan_resp_data_length);
}
return ad;
}
void test_gap_le_advert__single_job(void) {
const char ad_data[] = "ad data";
const char scan_resp_data[] = "scan resp data";
BLEAdData *ad = create_ad(ad_data, scan_resp_data);
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 160,
.max_interval_slots = 241,
.duration_secs = 10,
};
GAPLEAdvertisingJobRef job;
job = gap_le_advert_schedule(ad,
&advert_term,
sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job);
// Since there was nothing scheduled, expect that the ad data is set to the
// controller immediately:
cl_assert(gap_le_is_advertising_enabled());
// Check that the ad data that is passed to the controller is the same that
// was given when calling the API:
assert_ad_data(ad_data);
// Check that the scan resp data that is passed to the controller is the same
// that was given when calling the API:
Scan_Response_Data_t scan_resp_data_out;
cl_assert_equal_i(gap_le_get_scan_response_data(&scan_resp_data_out),
sizeof(scan_resp_data));
cl_assert(memcmp(&scan_resp_data_out, scan_resp_data,
sizeof(scan_resp_data)) == 0);
// Expect one regular timer to be running for advertisements:
cl_assert_equal_i(regular_timer_seconds_count(), 1);
// Unschedule callback should not have been called:
cl_assert_equal_i(s_unscheduled_cb_count, 0);
// Unschedule and expect not to be advertising any more:
gap_le_advert_unschedule(job);
cl_assert(!gap_le_is_advertising_enabled());
// Unschedule callback should have been called once:
cl_assert_equal_i(s_unscheduled_cb_count, 1);
cl_assert_equal_p(s_unscheduled_job, job);
cl_assert_equal_b(s_unscheduled_completed, false);
// Expect no advertisement timer
cl_assert_equal_i(regular_timer_seconds_count(), 0);
free(ad);
}
void test_gap_le_advert__single_job_multiple_terms_silence_and_loop_around(void) {
BLEAdData *ad = create_ad("yo", NULL);
GAPLEAdvertisingJobTerm advert_terms[] =
{
{
.min_interval_slots = 160,
.max_interval_slots = 240,
.duration_secs = 1,
},
{
.min_interval_slots = 320,
.max_interval_slots = 480,
.duration_secs = 1,
},
{
.min_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.max_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.duration_secs = 1,
},
{
.duration_secs = GAPLE_ADVERTISING_DURATION_LOOP_AROUND,
.loop_around_index = 1,
},
};
GAPLEAdvertisingJobRef job;
job = gap_le_advert_schedule(ad,
advert_terms,
sizeof(advert_terms)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job);
// First term:
cl_assert(gap_le_is_advertising_enabled());
assert_ad_data("yo");
gap_le_assert_advertising_interval(advert_terms[0].min_interval_slots,
advert_terms[0].max_interval_slots);
regular_timer_fire_seconds(1);
// Second term:
cl_assert(gap_le_is_advertising_enabled());
assert_ad_data("yo");
gap_le_assert_advertising_interval(advert_terms[1].min_interval_slots,
advert_terms[1].max_interval_slots);
regular_timer_fire_seconds(1);
// Silent term:
cl_assert(!gap_le_is_advertising_enabled());
regular_timer_fire_seconds(1);
// Looped around to second term (index==1):
cl_assert(gap_le_is_advertising_enabled());
assert_ad_data("yo");
gap_le_assert_advertising_interval(advert_terms[1].min_interval_slots,
advert_terms[1].max_interval_slots);
cl_assert_equal_i(s_unscheduled_cb_count, 0);
}
void test_gap_le_advert__single_job_multiple_terms(void) {
const char ad_data[] = "ad data";
const char scan_resp_data[] = "scan resp data";
BLEAdData *ad = create_ad(ad_data, scan_resp_data);
GAPLEAdvertisingJobTerm advert_terms[3] =
{
{
.min_interval_slots = 160,
.max_interval_slots = 240,
.duration_secs = 4,
},
{
.min_interval_slots = 320,
.max_interval_slots = 400,
.duration_secs = 4,
},
{
.min_interval_slots = 640,
.max_interval_slots = 800,
.duration_secs = 4,
},
};
GAPLEAdvertisingJobRef job;
job = gap_le_advert_schedule(ad,
advert_terms,
sizeof(advert_terms)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job);
// Since there was nothing scheduled, expect that the ad data is set to the
// controller immediately:
cl_assert(gap_le_is_advertising_enabled());
// Check that the ad data that is passed to the controller is the same that
// was given when calling the API:
assert_ad_data(ad_data);
// Check that the scan resp data that is passed to the controller is the same
// that was given when calling the API:
Scan_Response_Data_t scan_resp_data_out;
cl_assert_equal_i(gap_le_get_scan_response_data(&scan_resp_data_out),
sizeof(scan_resp_data));
cl_assert(memcmp(&scan_resp_data_out, scan_resp_data,
sizeof(scan_resp_data)) == 0);
// Expect one regular timer to be running for adverts:
cl_assert_equal_i(regular_timer_seconds_count(), 1);
// Unschedule callback should not have been called:
cl_assert_equal_i(s_unscheduled_cb_count, 0);
// Make sure the all the terms in the job are run:
for (int term = 0; term < ARRAY_LENGTH(advert_terms); ++term) {
for (int second_tick = 0; second_tick < 4; ++second_tick) {
cl_assert_equal_i(s_unscheduled_cb_count, 0);
assert_ad_data("ad data");
gap_le_assert_advertising_interval(advert_terms[term].min_interval_slots,
advert_terms[term].max_interval_slots);
regular_timer_fire_seconds(1);
}
}
cl_assert(!gap_le_is_advertising_enabled());
cl_assert_equal_i(s_unscheduled_cb_count, 1);
cl_assert_equal_p(s_unscheduled_job, job);
// Expect no advertisement timer
cl_assert_equal_i(regular_timer_seconds_count(), 0);
free(ad);
}
void test_gap_le_advert__job_round_robin(void) {
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = GAPLE_ADVERTISING_DURATION_INFINITE,
};
// Schedule infinite job "A":
BLEAdData *ad_a = create_ad("A", NULL);
GAPLEAdvertisingJobRef infinite_job_a;
infinite_job_a = gap_le_advert_schedule(ad_a,
&advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(infinite_job_a);
assert_ad_data("A");
// Schedule infinite job "B":
BLEAdData *ad_b = create_ad("B", NULL);
GAPLEAdvertisingJobRef infinite_job_b;
infinite_job_b = gap_le_advert_schedule(ad_b,
&advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(infinite_job_b);
assert_ad_data("B");
// Round-robin 10 times:
for (int i = 0; i < 10; ++i) {
regular_timer_fire_seconds(1);
assert_ad_data("A");
regular_timer_fire_seconds(1);
assert_ad_data("B");
}
// Introduce non-infinite job "C" for 10 seconds:
advert_term.duration_secs = 10;
BLEAdData *ad_c = create_ad("C", NULL);
GAPLEAdvertisingJobRef infinite_job_c;
infinite_job_c = gap_le_advert_schedule(ad_c,
&advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(infinite_job_c);
assert_ad_data("C");
// Round-robin 5 times:
for (int i = 0; i < 5; ++i) {
regular_timer_fire_seconds(1);
assert_ad_data("A");
regular_timer_fire_seconds(1);
assert_ad_data("B");
regular_timer_fire_seconds(1);
assert_ad_data("C");
}
// Introduce a second non-infinite job "D" for 10 seconds:
BLEAdData *ad_d = create_ad("D", NULL);
GAPLEAdvertisingJobRef infinite_job_d;
infinite_job_d = gap_le_advert_schedule(ad_d,
&advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
// It should get immediate air-time for one cycle:
cl_assert(infinite_job_d);
assert_ad_data("D");
// Round-robin 4 times:
for (int i = 0; i < 4; ++i) {
regular_timer_fire_seconds(1);
assert_ad_data("A");
regular_timer_fire_seconds(1);
assert_ad_data("B");
regular_timer_fire_seconds(1);
assert_ad_data("C");
regular_timer_fire_seconds(1);
assert_ad_data("D");
}
// Schedule yet another infinite job "E".
BLEAdData *ad_e = create_ad("E", NULL);
advert_term.duration_secs = GAPLE_ADVERTISING_DURATION_INFINITE;
GAPLEAdvertisingJobRef infinite_job_e;
infinite_job_e = gap_le_advert_schedule(ad_e,
&advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(infinite_job_e);
// Infinite jobs are equal in priority to finite jobs, so it should get immediate air-time for one cycle:
assert_ad_data("E");
// No jobs should been have been unscheduled:
cl_assert_equal_i(s_unscheduled_cb_count, 0);
// This is the last round for "C":
regular_timer_fire_seconds(1);
assert_ad_data("A");
regular_timer_fire_seconds(1);
assert_ad_data("B");
regular_timer_fire_seconds(1);
assert_ad_data("C");
regular_timer_fire_seconds(1);
assert_ad_data("D");
// One job ("C") should been have been unscheduled:
cl_assert_equal_i(s_unscheduled_cb_count, 1);
// Round-robin 5 times:
for (int i = 0; i < 5; ++i) {
regular_timer_fire_seconds(1);
assert_ad_data("E");
regular_timer_fire_seconds(1);
assert_ad_data("A");
regular_timer_fire_seconds(1);
assert_ad_data("B");
regular_timer_fire_seconds(1);
assert_ad_data("D");
}
// "D" should be done now, so expect only infinite jobs to get air-time again:
for (int i = 0; i < 10; ++i) {
regular_timer_fire_seconds(1);
assert_ad_data("E");
regular_timer_fire_seconds(1);
assert_ad_data("A");
regular_timer_fire_seconds(1);
assert_ad_data("B");
// Jobs "C" and "D" should have been unscheduled, infinite jobs should never
// get unscheduled:
cl_assert_equal_i(s_unscheduled_cb_count, 2);
cl_assert_equal_b(s_unscheduled_completed, true);
}
free(ad_a);
free(ad_b);
free(ad_c);
free(ad_d);
}
// Tests that the adv data is set when a job goes from a silent term to a non-silent one, and
// another job's data was previously advertised.
void test_gap_le_advert__data_set_after_silent_term(void) {
GAPLEAdvertisingJobTerm advert_terms_a[] =
{
{
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = 1,
},
};
GAPLEAdvertisingJobTerm advert_terms_b[] =
{
{
.min_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.max_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.duration_secs = 1,
},
{
.min_interval_slots = 32,
.max_interval_slots = 64,
.duration_secs = 1,
},
};
// Schedule job "A":
BLEAdData *ad_a = create_ad("A", NULL);
GAPLEAdvertisingJobRef job_a;
job_a = gap_le_advert_schedule(ad_a,
advert_terms_a,
sizeof(advert_terms_a)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job_a);
assert_ad_data("A");
regular_timer_fire_seconds(1);
cl_assert_equal_b(gap_le_is_advertising_enabled(), false);
cl_assert_equal_p(s_unscheduled_job, job_a);
// Schedule job "B":
BLEAdData *ad_b = create_ad("B", NULL);
GAPLEAdvertisingJobRef job_b;
job_b = gap_le_advert_schedule(ad_b,
advert_terms_b,
sizeof(advert_terms_b)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job_b);
cl_assert_equal_b(gap_le_is_advertising_enabled(), false);
regular_timer_fire_seconds(1);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
assert_ad_data("B");
}
void test_gap_le_advert__round_robin_two_jobs_incl_silent_terms(void) {
GAPLEAdvertisingJobTerm advert_terms_a[] =
{
{
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = 1,
},
{
.min_interval_slots = 480,
.max_interval_slots = 960,
.duration_secs = 1,
},
{
.min_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.max_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.duration_secs = 1,
},
{
.min_interval_slots = 256,
.max_interval_slots = 512,
.duration_secs = 1,
},
};
GAPLEAdvertisingJobTerm advert_terms_b[] =
{
{
.min_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.max_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.duration_secs = 1,
},
{
.min_interval_slots = 32,
.max_interval_slots = 64,
.duration_secs = 1,
},
{
.min_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.max_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS,
.duration_secs = 2,
},
{
.min_interval_slots = 960,
.max_interval_slots = 1240,
.duration_secs = 1,
},
};
// Schedule job "A":
BLEAdData *ad_a = create_ad("A", NULL);
GAPLEAdvertisingJobRef job_a;
job_a = gap_le_advert_schedule(ad_a,
advert_terms_a, sizeof(advert_terms_a)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job_a);
assert_ad_data("A");
// Schedule job "B":
BLEAdData *ad_b = create_ad("B", NULL);
GAPLEAdvertisingJobRef job_b;
job_b = gap_le_advert_schedule(ad_b,
advert_terms_b, sizeof(advert_terms_b)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
// Even though B is newer, expect "A" to be scheduled still, because B's first term is silent:
cl_assert(job_b);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
assert_ad_data("A");
gap_le_assert_advertising_interval(advert_terms_a[0].min_interval_slots,
advert_terms_a[0].max_interval_slots);
// After A's first term, expect that B's second term will follow. B's first term was silent, so
// this "ran" at the same time as A's first term.
regular_timer_fire_seconds(1);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
assert_ad_data("B");
gap_le_assert_advertising_interval(advert_terms_b[1].min_interval_slots,
advert_terms_b[1].max_interval_slots);
// Expect A's second term. B's third term (silent) will "run" now too.
regular_timer_fire_seconds(1);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
assert_ad_data("A");
gap_le_assert_advertising_interval(advert_terms_a[1].min_interval_slots,
advert_terms_a[1].max_interval_slots);
// Expect silence. B's third (silent) is 2 secs, so one more to go and A's third term is silent.
regular_timer_fire_seconds(1);
cl_assert_equal_b(gap_le_is_advertising_enabled(), false);
// Expect B's fourth term now.
regular_timer_fire_seconds(1);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
assert_ad_data("B");
gap_le_assert_advertising_interval(advert_terms_b[3].min_interval_slots,
advert_terms_b[3].max_interval_slots);
regular_timer_fire_seconds(1);
// Expect B to be done:
cl_assert_equal_i(s_unscheduled_cb_count, 1);
cl_assert_equal_p(s_unscheduled_job, job_b);
cl_assert_equal_b(s_unscheduled_completed, true);
// Expect A's fourth term:
assert_ad_data("A");
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
gap_le_assert_advertising_interval(advert_terms_a[3].min_interval_slots,
advert_terms_a[3].max_interval_slots);
regular_timer_fire_seconds(1);
// Expect A to be done as well:
cl_assert_equal_i(s_unscheduled_cb_count, 2);
cl_assert_equal_p(s_unscheduled_job, job_a);
cl_assert_equal_b(s_unscheduled_completed, true);
}
void test_gap_le_advert__job_round_robin_multiple_terms(void) {
GAPLEAdvertisingJobTerm advert_terms[2] =
{
{
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = 5,
},
{
.min_interval_slots = 480,
.max_interval_slots = 620,
.duration_secs = 5,
},
};
// Schedule job "A":
BLEAdData *ad_a = create_ad("A", NULL);
GAPLEAdvertisingJobRef job_a;
job_a = gap_le_advert_schedule(ad_a,
advert_terms, sizeof(advert_terms)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job_a);
assert_ad_data("A");
// Schedule job "B":
BLEAdData *ad_b = create_ad("B", NULL);
GAPLEAdvertisingJobRef job_b;
job_b = gap_le_advert_schedule(ad_b,
advert_terms, sizeof(advert_terms)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job_b);
assert_ad_data("B");
// Round-robin. Each term is 5 seconds, each job is 10 seconds:
for (int i = 0; i < 9; ++i) {
regular_timer_fire_seconds(1);
assert_ad_data("A");
cl_assert_equal_i(s_unscheduled_cb_count, 0);
regular_timer_fire_seconds(1);
assert_ad_data("B");
cl_assert_equal_i(s_unscheduled_cb_count, 0);
}
// Unschedule job "B", air job "A"
regular_timer_fire_seconds(1);
assert_ad_data("A");
cl_assert_equal_i(s_unscheduled_cb_count, 1);
cl_assert_equal_p(s_unscheduled_job, job_b);
cl_assert_equal_b(s_unscheduled_completed, true);
// Unschedule job "A"
regular_timer_fire_seconds(1);
cl_assert_equal_i(s_unscheduled_cb_count, 2);
cl_assert_equal_p(s_unscheduled_job, job_a);
cl_assert_equal_b(s_unscheduled_completed, true);
cl_assert(!gap_le_is_advertising_enabled());
free(ad_a);
free(ad_b);
}
void test_gap_le_advert__expiring_job(void) {
// Test that a job is expired correctly after the set duration:
uint16_t seconds_left = 5;
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = seconds_left,
};
BLEAdData *ad = create_ad(NULL, NULL);
GAPLEAdvertisingJobRef job;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job);
// No jobs should been have been unscheduled:
cl_assert_equal_i(s_unscheduled_cb_count, 0);
while (seconds_left) {
cl_assert(gap_le_is_advertising_enabled());
regular_timer_fire_seconds(1);
--seconds_left;
}
cl_assert(!gap_le_is_advertising_enabled());
cl_assert_equal_i(regular_timer_seconds_count(), 0);
// The job should been have been unscheduled:
cl_assert_equal_i(s_unscheduled_cb_count, 1);
cl_assert_equal_p(s_unscheduled_job, job);
cl_assert_equal_b(s_unscheduled_completed, true);
free(ad);
}
void test_gap_le_advert__invalid_params(void) {
GAPLEAdvertisingJobRef job;
BLEAdData *ad = create_ad(NULL, NULL);
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 31,
.max_interval_slots = 31,
.duration_secs = 1,
};
// Minimum interval boundary (no scan resp):
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
advert_term.min_interval_slots = 32;
advert_term.max_interval_slots = 32;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job);
// Minimum interval boundary (with scan resp):
char scan_resp_data[] = "scan resp data";
BLEAdData *ad_scannable = create_ad(NULL, scan_resp_data);
advert_term.min_interval_slots = 159;
advert_term.max_interval_slots = 159;
job = gap_le_advert_schedule(ad_scannable,
&advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
advert_term.min_interval_slots = 160;
advert_term.max_interval_slots = 160;
job = gap_le_advert_schedule(ad_scannable,
&advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job);
// Max < Min:
advert_term.min_interval_slots = 200;
advert_term.max_interval_slots = 32;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
// Loop-around in the first term:
advert_term.min_interval_slots = 200;
advert_term.max_interval_slots = 200;
advert_term.duration_secs = GAPLE_ADVERTISING_DURATION_LOOP_AROUND;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
// Forever silent term:
advert_term.min_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS;
advert_term.max_interval_slots = GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS;
advert_term.duration_secs = GAPLE_ADVERTISING_DURATION_INFINITE;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
// No terms:
job = gap_le_advert_schedule(ad, NULL, 0,
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
// No ad data:
advert_term.min_interval_slots = 200;
advert_term.max_interval_slots = 200;
advert_term.duration_secs = 1;
job = gap_le_advert_schedule(NULL, &advert_term,
sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
free(ad);
free(ad_scannable);
}
void test_gap_le_advert__unschedule_non_existent(void) {
// Unscheduling non-existent job should be fine, should not crash:
gap_le_advert_unschedule((GAPLEAdvertisingJobRef)(uintptr_t) 0x1234);
// Unschedule callback should not have been called:
cl_assert_equal_i(s_unscheduled_cb_count, 0);
}
void test_gap_le_advert__deinit_unschedules(void) {
BLEAdData *ad = create_ad(NULL, NULL);
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = 10,
};
GAPLEAdvertisingJobRef job;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert(job);
gap_le_advert_deinit();
cl_assert_equal_i(s_unscheduled_cb_count, 1);
cl_assert_equal_p(s_unscheduled_job, job);
cl_assert_equal_b(s_unscheduled_completed, false);
cl_assert_equal_i(regular_timer_seconds_count(), 0);
free(ad);
}
void test_gap_le_advert__cant_schedule_after_deinit(void) {
gap_le_advert_deinit();
BLEAdData *ad = create_ad(NULL, NULL);
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = 10,
};
GAPLEAdvertisingJobRef job;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_p(job, NULL);
cl_assert_equal_i(regular_timer_seconds_count(), 0);
}
void test_gap_le_advert__continue_after_slave_connection(void) {
BLEAdData *ad = create_ad(NULL, NULL);
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = 10,
};
GAPLEAdvertisingJobRef job;
job = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, 0);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
// Simulate stopping advertising because of inbound connection:
gap_le_set_advertising_disabled();
s_is_connected_as_slave = true;
// Call the connection handler:
gap_le_advert_handle_connect_as_slave();
// we should have stopped advertising for reconnection
cl_assert_equal_b(gap_le_is_advertising_enabled(), false);
// Trigger the advertising to start up again. Since we still have an advertisement job set,
// it should continue.
regular_timer_fire_seconds(1);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
free(ad);
}
void test_gap_le_advert__unschedule_job_types(void) {
BLEAdData *ad = create_ad(NULL, NULL);
GAPLEAdvertisingJobTerm advert_term = {
.min_interval_slots = 160,
.max_interval_slots = 320,
.duration_secs = 10,
};
GAPLEAdvertisingJobRef job_a;
job_a = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, GAPLEAdvertisingJobTagDiscovery);
GAPLEAdvertisingJobTag tag = GAPLEAdvertisingJobTagDiscovery;
gap_le_advert_unschedule_job_types(&tag, 1);
// make sure we can remove a tag when it is the only one scheduled
cl_assert(s_unscheduled_job == job_a);
cl_assert_equal_i(s_unscheduled_cb_count, 1);
// add back the job we just unscheduled
job_a = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, GAPLEAdvertisingJobTagDiscovery);
GAPLEAdvertisingJobRef job_b;
job_b = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, GAPLEAdvertisingJobTagReconnection);
GAPLEAdvertisingJobRef job_c;
job_c = gap_le_advert_schedule(ad, &advert_term, sizeof(advert_term)/sizeof(GAPLEAdvertisingJobTerm),
unscheduled_callback, s_unscheduled_cb_data, GAPLEAdvertisingJobTagReconnection);
// run some Ad cycling
for (int i = 0; i < 3; i++) {
regular_timer_fire_seconds(1);
}
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
// make sure we can remove a few jobs with the same advertisement type
tag = GAPLEAdvertisingJobTagReconnection;
gap_le_advert_unschedule_job_types(&tag, 1);
// should have 3 jobs unscheduled at this point and 1 still running
cl_assert_equal_i(s_unscheduled_cb_count, 3);
cl_assert_equal_b(gap_le_is_advertising_enabled(), true);
// make sure after we unschedule all the jobs no more are running
tag = GAPLEAdvertisingJobTagDiscovery;
gap_le_advert_unschedule_job_types(&tag, 1);
cl_assert_equal_i(s_unscheduled_cb_count, 4);
cl_assert_equal_b(gap_le_is_advertising_enabled(), false);
free(ad);
}
+871
View File
@@ -0,0 +1,871 @@
/*
* 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 "bluetooth/gap_le_connect.h"
#include "comm/ble/gap_le_connect.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/ble/gap_le_task.h"
#include "kernel/events.h"
#include "services/common/analytics/analytics.h"
#include "clar.h"
#include <bluetooth/bonding_sync.h>
#include <bluetooth/sm_types.h>
#include <btutil/bt_device.h>
// Fakes
///////////////////////////////////////////////////////////
#include "fake_events.h"
#include "fake_GAPAPI.h"
#include "fake_bluetooth_persistent_storage.h"
#include "fake_HCIAPI.h"
#include "fake_new_timer.h"
#include "fake_pbl_malloc.h"
#include "fake_system_task.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_lock.h"
#include "stubs_gap_le_advert.h"
#include "stubs_bluetooth_analytics.h"
#include "stubs_gatt_client_discovery.h"
#include "stubs_gatt_client_subscriptions.h"
#include "stubs_hexdump.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_pebble_pairing_service.h"
#include "stubs_regular_timer.h"
#include "stubs_shared_prf_storage.h"
// Note:
// The unit tests for "Pebble as Master" are disabled because role switching is not implemented yet,
// and the FW is currently "hard-wired" to be slave as a precautionary measure to prevent it from
// trying to connect as master. See PBL-20368.
void bt_driver_cb_handle_create_bonding(const BleBonding *bonding,
const BTDeviceAddress *addr) {
}
void cc2564A_bad_le_connection_complete_handle(unsigned int stack_id,
const GAP_LE_Current_Connection_Parameters_t *params) {
}
const GAP_LE_Pairing_Capabilities_t* gap_le_pairing_capabilities(void) {
return NULL;
}
void gap_le_device_name_request(uintptr_t stack_id, GAPLEConnection *connection) {
}
void gatt_service_changed_server_cleanup_by_connection(GAPLEConnection *connection) {
}
void bt_driver_handle_le_conn_params_update_event(
const BleConnectionUpdateCompleteEvent *event) {
}
typedef struct PairingUserConfirmationCtx PairingUserConfirmationCtx;
void bt_driver_pebble_pairing_service_handle_status_change(const GAPLEConnection *connection) {
}
void bt_driver_cb_pairing_confirm_handle_request(const PairingUserConfirmationCtx *ctx,
const char *device_name,
const char *confirmation_token) {
}
void bt_driver_cb_pairing_confirm_handle_completed(const PairingUserConfirmationCtx *ctx,
bool success) {
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
void analytics_event_bt_error(AnalyticsEvent type, uint32_t error) {
}
void bluetooth_analytics_handle_connection_disconnection_event(
AnalyticsEvent type, uint8_t reason, const BleRemoteVersionInfo *vers_info) {
}
void analytics_inc(AnalyticsMetric metric, AnalyticsClient client) {
}
// Helpers
///////////////////////////////////////////////////////////
static BTDeviceInternal prv_dummy_device(uint8_t octet) {
BTDeviceAddress address = {
.octets = {
[0] = octet,
[1] = octet,
[2] = octet,
[3] = octet,
[4] = octet,
[5] = octet,
},
};
BTDevice device = bt_device_init_with_address(address, true /* is_random */);
return *(BTDeviceInternal *)(&device);
}
static BTBondingID prv_add_bonding_for_fake_resolvable_device(void) {
BTDeviceInternal identity_device = {};
const SMIdentityResolvingKey *irk = (const SMIdentityResolvingKey *) fake_GAPAPI_get_fake_irk();
BleBonding bonding = {
.pairing_info = {
.identity = identity_device,
.irk = *irk,
.is_remote_identity_info_valid = true,
},
.is_gateway = true,
};
bt_driver_handle_host_added_bonding(&bonding);
return fake_bt_persistent_storage_add(irk, &identity_device, "Dummy", true /* is_gateway */);
}
static void prv_assert_no_event(void) {
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_NULL_EVENT);
}
static void prv_fake_connect(const BTDeviceInternal *device, bool is_master) {
// Simulate getting a Connection Complete event for the device from Bluetopia:
fake_gap_put_connection_event(HCI_ERROR_CODE_SUCCESS,
is_master, device);
cl_assert_equal_b(gap_le_connection_is_connected(device), true);
}
static void prv_fake_disconnect(const BTDeviceInternal *device, bool is_master) {
fake_gap_put_disconnection_event(HCI_ERROR_CODE_SUCCESS,
HCI_ERROR_CODE_CONNECTION_TERMINATED_BY_LOCAL_HOST,
is_master,
device);
cl_assert_equal_b(gap_le_connection_is_connected(device), false);
}
static void prv_assert_client_event(const BTDeviceInternal *device,
bool connected,
PebbleTaskBitset client_tasks,
uint8_t hci_reason) {
// Verify the Pebble event:
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_BLE_CONNECTION_EVENT);
// Event should only go to app:
cl_assert_equal_i(event.task_mask, (PebbleTaskBitset) ~(client_tasks));
const PebbleBLEConnectionEvent *conn_event = &event.bluetooth.le.connection;
const BTDeviceInternal event_device = PebbleEventToBTDeviceInternal(conn_event);
const bool is_same_device = bt_device_equal(&event_device.opaque,
&device->opaque);
cl_assert_equal_b(is_same_device, true);
cl_assert_equal_b(conn_event->connected, connected);
cl_assert_equal_i(conn_event->hci_reason, hci_reason);
}
// Tests
///////////////////////////////////////////////////////////
extern void gap_le_connect_bluetopia_connection_callback(
unsigned int stack_id, GAP_LE_Event_Data_t* event_data, unsigned long CallbackParameter);
void test_gap_le_connect__initialize(void) {
fake_GAPAPI_init();
// Register slave connection event callback for tests involving Pebble as slave:
// This normally happens in gap_le_advert.c. Taking a shortcut to avoid dragging in more code.
GAP_LE_Advertising_Enable(1, TRUE, NULL, NULL, gap_le_connect_bluetopia_connection_callback, 0);
fake_event_init();
fake_bt_persistent_storage_reset();
gap_le_connection_init();
gap_le_connect_init();
}
void test_gap_le_connect__cleanup(void) {
// Cancel all connection intents:
for (GAPLEClient c = 0; c < GAPLEClientNum; ++c) {
gap_le_connect_cancel_all(c);
}
gap_le_connect_deinit();
cl_assert_equal_b(gap_le_connect_has_pending_create_connection(), false);
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 0);
gap_le_connection_deinit();
cl_assert_equal_b(fake_HCIAPI_whitelist_error_count(), 0);
fake_HCIAPI_deinit();
}
// -----------------------------------------------------------------------------
// Parameter / Bounds checking
void test_gap_le_connect__register_max_intents(void) {
for (int i = 0; i < GAP_LE_CONNECT_MASTER_MAX_CONNECTION_INTENTS + 1; ++i) {
BTDeviceInternal device = prv_dummy_device(i);
BTErrno e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
if (i == GAP_LE_CONNECT_MASTER_MAX_CONNECTION_INTENTS) {
// When the limit is reached, expect "not enough resources" error:
cl_assert_equal_i(e, BTErrnoNotEnoughResources);
} else {
cl_assert_equal_i(e, BTErrnoOK);
const bool registered = gap_le_connect_has_connection_intent(&device,
GAPLEClientApp);
cl_assert_equal_b(registered, true);
}
}
}
void test_gap_le_connect__register_null_device(void) {
BTErrno e = gap_le_connect_connect(NULL,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
void test_gap_le_connect__unregister_null_device(void) {
BTErrno e = gap_le_connect_cancel(NULL, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
void test_gap_le_connect__register_invalid_bonding(void) {
BTErrno e = gap_le_connect_connect_by_bonding(BT_BONDING_ID_INVALID,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
void test_gap_le_connect__register_non_existing_bonding(void) {
BTErrno e = gap_le_connect_connect_by_bonding(~0,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
void test_gap_le_connect__unregister_invalid_bonding(void) {
BTErrno e = gap_le_connect_cancel_by_bonding(BT_BONDING_ID_INVALID, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
void test_gap_le_connect__unregister_non_existing_bonding(void) {
BTErrno e = gap_le_connect_cancel_by_bonding(~0, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
void test_gap_le_connect__register_is_already_registered_for_same_client(void) {
BTErrno e;
bool registered;
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
registered = gap_le_connect_has_connection_intent(&device, GAPLEClientApp);
cl_assert_equal_b(registered, true);
// Try registering the device again as same client:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidState);
// Should still be registered from the first call:
registered = gap_le_connect_has_connection_intent(&device, GAPLEClientApp);
cl_assert_equal_b(registered, true);
}
void test_gap_le_connect__register_same_device_and_bonding(void) {
// Test that it is possible to have 2 intents for the same device, when registering one intent
// using the resolvable address and one with a bonding. Pebble will not try to collate these,
// because there are many addresses that resolve to the same bonding. The current implementation
// uses one address or one bonding per intent.
BTErrno e;
bool registered;
BTDeviceInternal device = *fake_GAPAPI_get_device_resolving_to_fake_irk();
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
// Register connection intent:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
registered = gap_le_connect_has_connection_intent(&device, GAPLEClientApp);
cl_assert_equal_b(registered, true);
// Register another connection intent using the bonding:
e = gap_le_connect_connect_by_bonding(bonding_id,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
registered = gap_le_connect_has_connection_intent_for_bonding(bonding_id, GAPLEClientApp);
cl_assert_equal_b(registered, true);
}
void test_gap_le_connect__register_two_clients_same_device(void) {
BTErrno e;
bool registered;
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
registered = gap_le_connect_has_connection_intent(&device, GAPLEClientApp);
cl_assert_equal_b(registered, true);
// Try registering the device again for different client:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
// Assert registrations:
registered = gap_le_connect_has_connection_intent(&device, GAPLEClientApp);
cl_assert_equal_b(registered, true);
registered = gap_le_connect_has_connection_intent(&device, GAPLEClientKernel);
cl_assert_equal_b(registered, true);
// Only one registration (co-owned by the 2 clients):
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 1);
}
void test_gap_le_connect__unregister_unknown_device(void) {
BTDeviceInternal device = prv_dummy_device(1);
BTErrno e = gap_le_connect_cancel(&device, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
void test_gap_le_connect__unregister_unowned_intent(void) {
BTDeviceInternal device = prv_dummy_device(1);
BTErrno e;
// Register connection intent owned by kernel:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
// Unregister connection intent owned by app:
e = gap_le_connect_cancel(&device, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
}
// -----------------------------------------------------------------------------
// Virtual (dis)connection events
void __disabled_test_gap_le_connect__connection_event_for_registered_client(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent:
BTErrno e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Device isn't connected. Verify no event was caused as a result of the
// registration.
prv_assert_no_event();
// Connect & verify the client task gets the event:
prv_fake_connect(&device, true /* is_master*/);
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
// Disconnect & verify the client task gets the event:
prv_fake_disconnect(&device, true /* is_master */);
prv_assert_client_event(&device, false /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_CONNECTION_TERMINATED_BY_LOCAL_HOST);
}
void test_gap_le_connect__connection_event_for_registered_client_by_bonding(void) {
BTDeviceInternal device = *fake_GAPAPI_get_device_resolving_to_fake_irk();
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
// Register connection intent:
BTErrno e = gap_le_connect_connect_by_bonding(bonding_id,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Device isn't connected. Verify no event was caused as a result of the
// registration.
prv_assert_no_event();
// Connect & verify the client task gets the event:
prv_fake_connect(&device, false /* is_master*/);
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
// Disconnect & verify the client task gets the event:
prv_fake_disconnect(&device, false /* is_master */);
prv_assert_client_event(&device, false /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_CONNECTION_TERMINATED_BY_LOCAL_HOST);
}
void __disabled_test_gap_le_connect__register_for_already_connected_device(void) {
BTErrno e;
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent for kernel:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting a Connection Complete event for the device from Bluetopia:
prv_fake_connect(&device, true /* is_master*/);
// Verify the kernel task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_KernelMain),
HCI_ERROR_CODE_SUCCESS);
// Register connection intent for app:
e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Verify (only) the app task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
}
void test_gap_le_connect__register_for_already_connected_bonding(void) {
BTErrno e;
BTDeviceInternal device = *fake_GAPAPI_get_device_resolving_to_fake_irk();
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
// Register connection intent for kernel:
e = gap_le_connect_connect_by_bonding(bonding_id,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting a Connection Complete event for the device from Bluetopia:
prv_fake_connect(&device, false /* is_master*/);
// Verify the kernel task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_KernelMain),
HCI_ERROR_CODE_SUCCESS);
// Register connection intent for app:
e = gap_le_connect_connect_by_bonding(bonding_id,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Verify (only) the app task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
}
void __disabled_test_gap_le_connect__disconnection_event_upon_airplane_mode(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent:
BTErrno e = gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
prv_fake_connect(&device, true /* is_master*/);
// Air-plane mode:
gap_le_connect_deinit();
// Verify (only) the app task got a (virtual) connection event:
prv_assert_client_event(&device, false /* connected */, (1 << PebbleTask_App),
GAPLEConnectHCIReasonExtensionAirPlaneMode);
}
// -----------------------------------------------------------------------------
// Auto-reconnect Tests
void __disabled_test_gap_le_connect__single_client_no_autoreconnect(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
prv_fake_connect(&device, true /* is_master*/);
// Verify the app task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
prv_fake_disconnect(&device, true /* is_master */);
// Verify the app task got a (virtual) disconnection event:
prv_assert_client_event(&device, false /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_CONNECTION_TERMINATED_BY_LOCAL_HOST);
// Verify that the connection intent has been removed after disconnection:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 0);
}
void __disabled_test_gap_le_connect__two_clients_one_without_autoreconnect(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register auto-reconnecting connection intent for kernel:
gap_le_connect_connect(&device,
true /* auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientKernel);
prv_fake_connect(&device, true /* is_master*/);
// Register one-shot connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
prv_fake_disconnect(&device, true /* is_master */);
// Verify both app task and kernel got the (virtual) disconnection event:
prv_assert_client_event(&device, false /* connected */,
(1 << PebbleTask_App) | (1 << PebbleTask_KernelMain),
HCI_ERROR_CODE_CONNECTION_TERMINATED_BY_LOCAL_HOST);
// Verify that the connection intent is still there for the Kernel:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 1);
cl_assert_equal_b(gap_le_connect_has_connection_intent(&device, GAPLEClientKernel), true);
cl_assert_equal_b(gap_le_connect_has_connection_intent(&device, GAPLEClientApp), false);
cl_assert_equal_b(gap_le_connect_has_pending_create_connection(), true);
}
// -----------------------------------------------------------------------------
// Cancel Connect (as Master)
void __disabled_test_gap_le_connect__cancel_connect(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
BTErrno e = gap_le_connect_cancel(&device, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// The LE Cancel Create Connection command is always followed by an event that
// is sent by the BT Controller. Simulate this event:
fake_gap_le_put_cancel_create_event(&device, true /* is_master */);
prv_assert_no_event();
// Verify there are no more intents:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 0);
cl_assert_equal_b(gap_le_connect_has_pending_create_connection(), false);
}
void __disabled_test_gap_le_connect__disconnection_event_upon_cancel_connect(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
prv_fake_connect(&device, false /* is_master*/);
BTErrno e = gap_le_connect_cancel(&device, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// The LE Cancel Create Connection command is always followed by an event that
// is sent by the BT Controller. Simulate this event:
fake_gap_le_put_cancel_create_event(&device, true /* is_master */);
// Verify the app task got a (virtual) disconnection event:
prv_assert_client_event(&device, false /* connected */, (1 << PebbleTask_App),
GAPLEConnectHCIReasonExtensionCancelConnect);
// Verify there are no more intents:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 0);
}
// -----------------------------------------------------------------------------
// Cancel Connect by Bonding (as Slave)
void test_gap_le_connect__slave_cancel_connect_by_bonding(void) {
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
// Register connection intent for app:
gap_le_connect_connect_by_bonding(bonding_id,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
BTErrno e = gap_le_connect_cancel_by_bonding(bonding_id, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
prv_assert_no_event();
// Verify there are no more intents:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 0);
cl_assert_equal_b(gap_le_connect_has_pending_create_connection(), false);
}
void test_gap_le_connect__slave_disconnection_event_upon_cancel_connect_by_bonding(void) {
BTDeviceInternal device = *fake_GAPAPI_get_device_resolving_to_fake_irk();
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
prv_fake_connect(&device, false /* is_master*/);
// Register connection intent for app:
gap_le_connect_connect_by_bonding(bonding_id,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
BTErrno e = gap_le_connect_cancel_by_bonding(bonding_id, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Verify the app task got a (virtual) disconnection event:
prv_assert_client_event(&device, false /* connected */, (1 << PebbleTask_App),
GAPLEConnectHCIReasonExtensionCancelConnect);
// Verify there are no more intents:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 0);
}
// -------------------------------------------------------------------------------------------------
// Pairing
void __disabled_test_gap_le_connect__one_shot_intent_removed_when_disconnected_before_encrpt(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection one-shot intent, with pairing required:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
true /* is_pairing_required */,
GAPLEClientKernel);
// Expect intent:
cl_assert_equal_b(gap_le_connect_has_connection_intent(&device, GAPLEClientKernel), true);
prv_fake_connect(&device, true /* is_master*/);
prv_fake_disconnect(&device, true /* is_master */);
// Expect intent to be removed:
cl_assert_equal_b(gap_le_connect_has_connection_intent(&device, GAPLEClientKernel), false);
}
void test_gap_le_connect__connection_event_only_after_encrypted_if_encryption_required(void) {
BTDeviceInternal device = *fake_GAPAPI_get_device_resolving_to_fake_irk();
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
// Register connection intent for app:
gap_le_connect_connect_by_bonding(bonding_id,
true /* no auto_reconnect */,
true /* is_pairing_required */,
GAPLEClientApp);
prv_fake_connect(&device, false /* is_master*/);
// Verify the app task got NO (virtual) connection event, the link is not encrypted yet:
// TODO: legacy PEBBLE_BT_CONNECTION_EVENT is still emitted, see gap_le_connect.c
// prv_put_legacy_connection_event.
//
// prv_assert_no_event();
fake_event_clear_last();
fake_GAPAPI_set_encrypted_for_device(&device);
fake_GAPAPI_put_encryption_change_event(true /* encrypted */, GAP_LE_PAIRING_STATUS_NO_ERROR,
false /* is_master */, &device);
// Verify the app task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
}
void test_gap_le_connect__add_intent_requiring_pairing_after_connected_and_encrypted(void) {
BTDeviceInternal device = *fake_GAPAPI_get_device_resolving_to_fake_irk();
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
prv_fake_connect(&device, false /* is_master*/);
fake_GAPAPI_set_encrypted_for_device(&device);
fake_GAPAPI_put_encryption_change_event(true /* encrypted */, GAP_LE_PAIRING_STATUS_NO_ERROR,
false /* is_master */, &device);
fake_event_clear_last();
const BleAddressAndIRKChange e = {
.device = device,
.is_address_updated = true,
.new_device = device,
.is_resolved = true,
.irk = *(const SMIdentityResolvingKey *) fake_GAPAPI_get_fake_irk(),
};
bt_driver_handle_le_connection_handle_update_address_and_irk(&e);
gap_le_connection_by_device(&device);
// Register connection intent for app:
gap_le_connect_connect_by_bonding(bonding_id,
true /* no auto_reconnect */,
true /* is_pairing_required */,
GAPLEClientApp);
// Verify the app task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
}
// -----------------------------------------------------------------------------
// Handling Bonding Changes
void test_gap_le_connect__removed_bonding_while_connected(void) {
BTDeviceInternal device = *fake_GAPAPI_get_device_resolving_to_fake_irk();
BTBondingID bonding_id = prv_add_bonding_for_fake_resolvable_device();
// Register connection intent for app:
gap_le_connect_connect_by_bonding(bonding_id,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
prv_fake_connect(&device, false /* is_master*/);
// Verify the app task got a (virtual) connection event:
prv_assert_client_event(&device, true /* connected */, (1 << PebbleTask_App),
HCI_ERROR_CODE_SUCCESS);
// Simulate "bonding will delete" callback:
gap_le_connect_handle_bonding_change(bonding_id, BtPersistBondingOpWillDelete);
// Verify the app task got a (virtual) disconnection event:
prv_assert_client_event(&device, false /* connected */, (1 << PebbleTask_App),
GAPLEConnectHCIReasonExtensionUserRemovedBonding);
// Verify there are no more intents:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 0);
}
// -----------------------------------------------------------------------------
// BT Controller White-list management
void __disabled_test_gap_le_connect__whitelist_add_when_disconnected(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
// Not connected yet, so expect to be added to white-list:
cl_assert_equal_b(fake_HCIAPI_whitelist_contains(&device), true);
prv_fake_connect(&device, true /* is_master*/);
// Connected, so expect to be removed from white-list:
cl_assert_equal_b(fake_HCIAPI_whitelist_contains(&device), false);
}
void __disabled_test_gap_le_connect__whitelist_add_when_connected(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientKernel);
prv_fake_connect(&device, true /* is_master*/);
// Register connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
// Connected, so expect to be removed from white-list:
cl_assert_equal_b(fake_HCIAPI_whitelist_contains(&device), false);
}
void __disabled_test_gap_le_connect__whitelist_remove_when_connected(void) {
BTDeviceInternal device = prv_dummy_device(1);
// Register connection intent for app:
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
prv_fake_connect(&device, true /* is_master*/);
gap_le_connect_cancel(&device, GAPLEClientApp);
// Connected, so expect to be removed from white-list:
cl_assert_equal_b(fake_HCIAPI_whitelist_contains(&device), false);
}
void __disabled_test_gap_le_connect__whitelist_repopulated_on_init(void) {
BTDeviceInternal device = prv_dummy_device(1);
gap_le_connect_connect(&device,
false /* no auto_reconnect */,
false /* is_pairing_required */,
GAPLEClientApp);
gap_le_connect_deinit();
// Connection intents survive air-plane mode:
cl_assert_equal_i(gap_le_connect_connection_intents_count(), 1);
// "Reset" BT Controller:
fake_HCIAPI_deinit();
cl_assert_equal_i(fake_HCIAPI_whitelist_count(), 0);
gap_le_connect_init();
// Not connected yet, so expect to be added to white-list:
cl_assert_equal_b(fake_HCIAPI_whitelist_contains(&device), true);
cl_assert_equal_i(fake_HCIAPI_whitelist_count(), 1);
}
+319
View File
@@ -0,0 +1,319 @@
/*
* 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 <bluetooth/gatt.h>
#include "comm/ble/gatt_client_accessors.h"
#include "comm/ble/gatt_client_discovery.h"
#include "comm/ble/gatt_service_changed.h"
#include "comm/ble/gap_le_connection.h"
#include "kernel/events.h"
#include "clar.h"
#include <btutil/bt_device.h>
#include <btutil/bt_uuid.h>
// Fakes
///////////////////////////////////////////////////////////
#include "fake_GAPAPI.h"
#include "fake_GATTAPI.h"
#include "fake_GATTAPI_test_vectors.h"
#include "fake_events.h"
#include "fake_new_timer.h"
#include "fake_system_task.h"
#include "fake_pbl_malloc.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_driver_gatt.h"
#include "stubs_bt_lock.h"
#include "stubs_gatt_client_subscriptions.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_prompt.h"
#include "stubs_rand_ptr.h"
#include "stubs_regular_timer.h"
void core_dump_reset(bool is_forced) {
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
uint16_t gaps_get_starting_att_handle(void) {
return 4;
}
// Helpers
///////////////////////////////////////////////////////////
#define TEST_GATT_CONNECTION_ID (1234)
static BTDeviceInternal prv_dummy_device(uint8_t octet) {
BTDeviceAddress address = {
.octets = {
[0] = octet,
[1] = octet,
[2] = octet,
[3] = octet,
[4] = octet,
[5] = octet,
},
};
BTDevice device = bt_device_init_with_address(address, true /* is_random */);
return *(BTDeviceInternal *)(&device);
}
static BTDeviceInternal prv_connected_dummy_device(uint8_t octet) {
BTDeviceInternal device = prv_dummy_device(octet);
gap_le_connection_add(&device, NULL, true /* local_is_master */);
GAPLEConnection *connection = gap_le_connection_by_device(&device);
connection->gatt_connection_id = TEST_GATT_CONNECTION_ID;
return device;
}
static void prv_mock_put_service_discovery_events(void) {
// Simulate discovery of Blood Pressure service:
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_indication_health_thermometer_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_indication_random_128bit_uuid_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
}
// Tests
///////////////////////////////////////////////////////////
void test_gatt_client_accessors__initialize(void) {
fake_gatt_init();
fake_event_init();
gap_le_connection_init();
}
void test_gatt_client_accessors__cleanup(void) {
gap_le_connection_deinit();
}
void test_gatt_client_accessors__copy_service_refs(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_mock_put_service_discovery_events();
const Service *bp_service = fake_gatt_get_blood_pressure_service();
const Service *thermo_service = fake_gatt_get_health_thermometer_service();
const Service *random_128bit_service = fake_gatt_get_random_128bit_uuid_service();
const Service *services[] = {
bp_service,
thermo_service,
random_128bit_service,
};
const uint8_t num_services = 3;
// Test gatt_client_copy_service_refs():
BLEService service_refs[num_services];
uint8_t num_found_services = gatt_client_copy_service_refs(&device, service_refs, num_services);
cl_assert_equal_i(num_found_services, num_services);
for (uint8_t s = 0; s < num_found_services; ++s) {
BLEService service_ref = service_refs[s];
const Service *expected_service = services[s];
// Test gatt_client_service_get_uuid():
const Uuid uuid = gatt_client_service_get_uuid(service_ref);
cl_assert(uuid_equal(&uuid, &expected_service->uuid));
// Test gatt_client_service_get_device():
BTDeviceInternal returned_device = gatt_client_service_get_device(service_ref);
cl_assert(bt_device_equal(&returned_device.opaque, &device.opaque));
// Check Characteristics:
const uint8_t num_characteristics = expected_service->num_characteristics;
BLECharacteristic characteristic_refs[num_characteristics];
// Test gatt_client_service_get_characteristics():
const uint8_t num_found_characteristics =
gatt_client_service_get_characteristics(service_ref, characteristic_refs, num_characteristics);
cl_assert_equal_i(num_characteristics, num_found_characteristics);
for (uint8_t c = 0; c < num_found_characteristics; ++c) {
BLECharacteristic characteristic_ref = characteristic_refs[c];
const Characteristic *expected_characteristic = &expected_service->characteristics[c];
// Test gatt_client_characteristic_get_uuid():
const Uuid uuid = gatt_client_characteristic_get_uuid(characteristic_ref);
cl_assert(uuid_equal(&uuid, &expected_characteristic->uuid));
// Test gatt_client_characteristic_get_properties():
cl_assert_equal_i(gatt_client_characteristic_get_properties(characteristic_ref),
expected_characteristic->properties);
// Test gatt_client_characteristic_get_service():
cl_assert_equal_i(gatt_client_characteristic_get_service(characteristic_ref), service_ref);
// Test gatt_client_characteristic_get_device():
BTDeviceInternal returned_device = gatt_client_characteristic_get_device(characteristic_ref);
cl_assert(bt_device_equal(&returned_device.opaque, &device.opaque));
// Test gatt_client_characteristic_get_descriptors():
const uint8_t num_descriptors = expected_characteristic->num_descriptors;
BLEDescriptor descriptor_refs[num_descriptors];
const uint8_t num_found_descriptors =
gatt_client_characteristic_get_descriptors(characteristic_ref, descriptor_refs, num_descriptors);
cl_assert_equal_i(num_descriptors, num_found_descriptors);
for (uint8_t d = 0; d < num_descriptors; ++d) {
const Descriptor *expected_descriptor = &expected_characteristic->descriptors[d];
const BLEDescriptor descriptor_ref = descriptor_refs[d];
// Test gatt_client_descriptor_get_uuid():
const Uuid uuid = gatt_client_descriptor_get_uuid(descriptor_ref);
cl_assert(uuid_equal(&uuid, &expected_descriptor->uuid));
// Test gatt_client_descriptor_get_characteristic():
cl_assert_equal_i(gatt_client_descriptor_get_characteristic(descriptor_ref), characteristic_ref);
}
}
// Test gatt_client_service_get_included_services():
const uint8_t num_inc_services = expected_service->num_included_services;
BLEService inc_service_refs[num_inc_services];
const uint8_t num_found_included_services =
gatt_client_service_get_included_services(service_ref, inc_service_refs, num_inc_services);
cl_assert_equal_i(num_inc_services, num_found_included_services);
for (uint8_t i = 0; i < num_inc_services; ++i) {
const Service *expected_inc_service = expected_service->included_services[i];
BLEService inc_service = inc_service_refs[i];
// Only check the Service UUID:
const Uuid uuid = gatt_client_service_get_uuid(inc_service);
cl_assert(uuid_equal(&uuid, &expected_inc_service->uuid));
}
}
}
void test_gatt_client_accessors__copy_service_refs_matching(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_mock_put_service_discovery_events();
const uint8_t num_services = 1;
// Test gatt_client_copy_service_refs():
BLEService service_refs[num_services];
const Service *bp_service = fake_gatt_get_blood_pressure_service();
uint8_t num_found_services = gatt_client_copy_service_refs_matching_uuid(&device, service_refs,
num_services,
&bp_service->uuid);
cl_assert_equal_i(num_found_services, num_services);
// Test that the UUID matches the Blood Pressure UUID:
const Uuid uuid = gatt_client_service_get_uuid(service_refs[0]);
cl_assert(uuid_equal(&uuid, &bp_service->uuid));
}
void test_gatt_client_accessors__get_characteristics_matching_uuids(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_mock_put_service_discovery_events();
const Service *bp_service = fake_gatt_get_blood_pressure_service();
// Get the reference to the Blood Pressure service:
const uint8_t num_services = 1;
BLEService service_refs[num_services];
gatt_client_copy_service_refs_matching_uuid(&device, service_refs,
num_services, &bp_service->uuid);
Uuid matching_uuids[3];
matching_uuids[0] = bp_service->characteristics[1].uuid;
matching_uuids[1] = bt_uuid_expand_16bit(0xffff); // not expected to match
matching_uuids[2] = bp_service->characteristics[0].uuid;
BLECharacteristic characteristics[3];
const uint8_t found =
gatt_client_service_get_characteristics_matching_uuids(service_refs[0], characteristics,
matching_uuids, 3);
cl_assert_equal_i(found, 2);
// Expect the order of the matching_uuids array is preserved:
cl_assert(uuid_equal(&matching_uuids[0], &bp_service->characteristics[1].uuid));
cl_assert(uuid_equal(&matching_uuids[2], &bp_service->characteristics[0].uuid));
// Expect the 0xffff UUID to return "no match":
cl_assert_equal_i(characteristics[1], BLE_CHARACTERISTIC_INVALID);
}
extern uint8_t gatt_client_copy_service_refs_by_discovery_generation(
const BTDeviceInternal *device, BLEService services_out[],
uint8_t num_services, uint8_t discovery_gen);
extern void gatt_client_discovery_discover_range(GAPLEConnection *connection,
ATTHandleRange *hdl_range);
void test_gatt_client_accessors__get_service_refs_by_discovery_gen(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS, TEST_GATT_CONNECTION_ID);
ATTHandleRange range = {
.start = 0x1,
.end = 0xC000,
};
gatt_client_discovery_discover_range(gap_le_connection_by_device(&device), &range);
fake_gatt_put_discovery_indication_health_thermometer_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_indication_random_128bit_uuid_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS, TEST_GATT_CONNECTION_ID);
const Service *bp_service = fake_gatt_get_blood_pressure_service();
const Service *thermo_service = fake_gatt_get_health_thermometer_service();
const Service *random_128bit_service = fake_gatt_get_random_128bit_uuid_service();
BLEService service_refs_out[3];
// Only the BP service should be part of the first generation
uint8_t refs_out = gatt_client_copy_service_refs_by_discovery_generation(
&device, service_refs_out, 3, 0);
cl_assert_equal_i(1, refs_out);
const Uuid uuid = gatt_client_service_get_uuid(service_refs_out[0]);
cl_assert(uuid_equal(&uuid, &bp_service->uuid));
// Thermo & Random 128 bit service should be part of the second gen
refs_out = gatt_client_copy_service_refs_by_discovery_generation(
&device, service_refs_out, 3, 1);
cl_assert_equal_i(2, refs_out);
const Uuid uuid1 = gatt_client_service_get_uuid(service_refs_out[0]);
const Uuid uuid2 = gatt_client_service_get_uuid(service_refs_out[0]);
cl_assert(uuid_equal(&uuid1, &thermo_service->uuid) ||
uuid_equal(&uuid1, &random_128bit_service->uuid));
cl_assert(uuid_equal(&uuid2, &thermo_service->uuid) ||
uuid_equal(&uuid2, &random_128bit_service->uuid));
}
+520
View File
@@ -0,0 +1,520 @@
/*
* 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 "comm/ble/gatt_client_discovery.h"
#include "comm/ble/gatt_service_changed.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/ble/gap_le_task.h"
#include "kernel/events.h"
#include "clar.h"
#include <btutil/bt_device.h>
// Fakes
///////////////////////////////////////////////////////////
#include "fake_GAPAPI.h"
#include "fake_GATTAPI.h"
#include "fake_GATTAPI_test_vectors.h"
#include "fake_events.h"
#include "fake_new_timer.h"
#include "fake_pbl_malloc.h"
#include "fake_system_task.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_driver_gatt.h"
#include "stubs_bt_lock.h"
#include "stubs_gatt_client_subscriptions.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_rand_ptr.h"
#include "stubs_regular_timer.h"
void core_dump_reset(bool is_forced) {
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
uint16_t gaps_get_starting_att_handle(void) {
return 4;
}
// Helpers
///////////////////////////////////////////////////////////
extern TimerID bt_driver_gatt_get_watchdog_timer_id(void);
#define TEST_GATT_CONNECTION_ID (1234)
static BTDeviceInternal prv_dummy_device(uint8_t octet) {
BTDeviceAddress address = {
.octets = {
[0] = octet,
[1] = octet,
[2] = octet,
[3] = octet,
[4] = octet,
[5] = octet,
},
};
BTDevice device = bt_device_init_with_address(address, true /* is_random */);
return *(BTDeviceInternal *)(&device);
}
static BTDeviceInternal prv_connected_dummy_device(uint8_t octet) {
BTDeviceInternal device = prv_dummy_device(octet);
gap_le_connection_add(&device, NULL, true /* local_is_master */);
GAPLEConnection *connection = gap_le_connection_by_device(&device);
connection->gatt_connection_id = TEST_GATT_CONNECTION_ID;
return device;
}
static void prv_assert_no_event(void) {
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_NULL_EVENT);
}
static void prv_assert_event(const BTDeviceInternal *device, BTErrno status) {
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_BLE_GATT_CLIENT_EVENT);
cl_assert_equal_i(event.bluetooth.le.gatt_client_service.subtype,
PebbleBLEGATTClientEventTypeServiceChange);
cl_assert_equal_i(event.bluetooth.le.gatt_client_service.info->status,
status);
const BTDeviceInternal event_device = event.bluetooth.le.gatt_client_service.info->device;
const bool equal_devices = bt_device_equal(&device->opaque,
&event_device.opaque);
cl_assert_equal_b(equal_devices, true);
// clear the event
fake_event_clear_last();
fake_event_reset_count();
}
static void prv_simulate_and_assert_discovery_of_one_service(const BTDeviceInternal *device) {
// Simulate discovery of Blood Pressure service:
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
prv_assert_event(device, BTErrnoOK);
}
// Tests
///////////////////////////////////////////////////////////
void test_gatt_client_discovery__initialize(void) {
fake_gatt_init();
fake_event_init();
gap_le_connection_init();
}
void test_gatt_client_discovery__cleanup(void) {
gap_le_connection_deinit();
// make sure we haven't leaked any memory!
fake_pbl_malloc_check_net_allocs();
}
// -----------------------------------------------------------------------------
// Edge cases
void test_gatt_client_discovery__not_connected(void) {
BTDeviceInternal device = prv_dummy_device(1);
cl_assert_equal_i(gatt_client_discovery_discover_all(&device),
BTErrnoInvalidParameter);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
}
void test_gatt_client_discovery__already_in_progress(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery for device:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
// Start again (expect to fail):
cl_assert_equal_i(gatt_client_discovery_discover_all(&device),
BTErrnoInvalidState);
// take down the connection and a disconnection event should be emitted
gap_le_connection_remove(&device);
prv_assert_event(&device, BTErrnoServiceDiscoveryDisconnected);
}
void test_gatt_client_discovery__event_is_sent_when_already_discovered(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
// Simulate discovery of 1 service:
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
prv_assert_event(&device, BTErrnoOK);
fake_event_clear_last();
// Start discovery again, expect not to run (already discovered):
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
// Expect event:
prv_assert_event(&device, BTErrnoOK);
}
void test_gatt_client_discovery__disconnected_during_discovery(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Simulate disconnection:
gap_le_connection_remove(&device);
// Process racing discovery indication:
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
// Bluetopia's GATT module does *NOT* emit a service discovery completion event for disconnections
// Test that our API *does* emit an service discovery event with "disconnected" reason:
prv_assert_event(&device, BTErrnoServiceDiscoveryDisconnected);
}
void test_gatt_client_discovery__complete_error(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Simulate getting one service indication...
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
// ... then a failure:
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_RESPONSE_TIMEOUT,
TEST_GATT_CONNECTION_ID);
// Expect event with error status and 0 services:
prv_assert_event(&device,
BTErrnoWithBluetopiaError(GATT_SERVICE_DISCOVERY_STATUS_RESPONSE_TIMEOUT));
// Expect service discovery to be stopped:
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
}
// -------------------------------------------------------------------------------------------------
// Watchdog timeout tests
static void prv_fire_watchdog_timeouts(const BTDeviceInternal *device, int retries) {
for (int i = 0; i <= retries; ++i) {
const int start_count = fake_gatt_is_service_discovery_start_count();
const int stop_count = fake_gatt_is_service_discovery_stop_count();
// Fire the watchdog timer:
const TimerID watchdog_timer = bt_driver_gatt_get_watchdog_timer_id();
stub_new_timer_fire(watchdog_timer);
// Check whether GATT_Stop_Service_Discovery has been called:
cl_assert_equal_i(stop_count + 1, fake_gatt_is_service_discovery_stop_count());
if (i < GATT_CLIENT_DISCOVERY_MAX_RETRY) {
// Check whether GATT_Start_Service_Discovery has been called, except for the last iteration:
cl_assert_equal_i(start_count + 1, fake_gatt_is_service_discovery_start_count());
// No client event:
prv_assert_no_event();
} else {
// Last iteration: expect event with error status and 0 services:
prv_assert_event(device, BTErrnoServiceDiscoveryTimeout);
}
}
}
void test_gatt_client_discovery__watchdog_error_out_after_max_retries(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_fire_watchdog_timeouts(&device, GATT_CLIENT_DISCOVERY_MAX_RETRY);
}
void test_gatt_client_discovery__watchdog_retry_counter_not_affecting_successive_process(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_fire_watchdog_timeouts(&device, GATT_CLIENT_DISCOVERY_MAX_RETRY);
// Make sure the previous retry counter doesn't affect any new discovery process:
fake_event_clear_last();
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Fire watchdog one less time than the maximum:
prv_fire_watchdog_timeouts(&device, GATT_CLIENT_DISCOVERY_MAX_RETRY - 1);
// Finally make the Bluetopia discovery events come in:
prv_simulate_and_assert_discovery_of_one_service(&device);
}
void test_gatt_client_discovery__watchdog_race_with_stopping(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Make Bluetopia's GATT_Stop_Service_Discovery fail:
// (Service discovery has finished in the mean time, disconnected, ...)
fake_gatt_set_stop_return_value(BTGATT_ERROR_INVALID_PARAMETER);
// Fire the watchdog timer:
const TimerID watchdog_timer = bt_driver_gatt_get_watchdog_timer_id();
stub_new_timer_fire(watchdog_timer);
// No event should be generated, because the finishing / disconnecting / ... should cause
// Bluetopia to call back to prv_handle_bluetopia_discovery_event() and therefore the normal
// path will be taken.
prv_assert_no_event();
// take down the connection and a disconnection event should be emitted
gap_le_connection_remove(&device);
prv_assert_event(&device, BTErrnoServiceDiscoveryDisconnected);
}
void test_gatt_client_discovery__watchdog_race_with_restarting(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Make Bluetopia's GATT_Start_Service_Discovery fail:
// (Disconnected in the mean time, ...)
fake_gatt_set_start_return_value(BTGATT_ERROR_INVALID_PARAMETER);
// Fire the watchdog timer:
const TimerID watchdog_timer = bt_driver_gatt_get_watchdog_timer_id();
stub_new_timer_fire(watchdog_timer);
// Stopping did not fail, but restarting did. In this case we need to generate an event that
// the discovery process failed. The error from GATT_Start_Service_Discovery is expected to be
// passed in the event.
prv_assert_event(&device, BTErrnoWithBluetopiaError(BTGATT_ERROR_INVALID_PARAMETER));
}
// -------------------------------------------------------------------------------------------------
// Re-discovery
extern BTErrno gatt_client_discovery_rediscover_all(const BTDeviceInternal *device);
void test_gatt_client_discovery__rediscover_not_already_running(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_simulate_and_assert_discovery_of_one_service(&device);
GAPLEConnection *connection = gap_le_connection_by_gatt_id(TEST_GATT_CONNECTION_ID);
// Expect one service, Blood Pressure:
cl_assert_equal_i(list_count(&connection->gatt_remote_services->node), 1);
fake_event_clear_last();
// Re-discovery:
cl_assert_equal_i(gatt_client_discovery_rediscover_all(&device), BTErrnoOK);
// Expect "Database Changed" event:
prv_assert_event(&device, BTErrnoServiceDiscoveryDatabaseChanged);
// Expect all services nodes to be cleaned up:
cl_assert_equal_i(list_count(&connection->gatt_remote_services->node), 0);
// Put one, expect one:
prv_simulate_and_assert_discovery_of_one_service(&device);
cl_assert_equal_i(list_count(&connection->gatt_remote_services->node), 1);
}
void test_gatt_client_discovery__rediscover_already_running(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
// Put one service, but do not finish...
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
prv_assert_no_event();
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
const int stop_count_before_rediscovery = fake_gatt_is_service_discovery_stop_count();
// Re-discovery:
cl_assert_equal_i(gatt_client_discovery_rediscover_all(&device), BTErrnoOK);
// Assert the previous process has been stopped:
cl_assert_equal_i(stop_count_before_rediscovery + 1, fake_gatt_is_service_discovery_stop_count());
// Expect "Database Changed" event:
prv_assert_event(&device, BTErrnoServiceDiscoveryDatabaseChanged);
// Put one, expect one:
prv_simulate_and_assert_discovery_of_one_service(&device);
}
extern void gatt_client_discovery_discover_range(GAPLEConnection *connection,
ATTHandleRange *hdl_range);
void test_gatt_client_discovery__multiple_jobs_pending(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
GAPLEConnection *connection = gap_le_connection_by_device(&device);
ATTHandleRange range = {
.start = 0x1,
.end = 0x3000
};
ATTHandleRange range_alt = {
.start = 0x3001,
.end = 0x4000
};
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
// start a discovery job, pretend nothing is found
gatt_client_discovery_discover_range(connection, &range);
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
// pend up another service discovery job
gatt_client_discovery_discover_range(connection, &range);
cl_assert_equal_i(1, fake_gatt_is_service_discovery_start_count());
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
// Nothing was found so we should just have a completion event
cl_assert_equal_i(1, fake_event_get_count());
prv_assert_event(&device, BTErrnoOK);
cl_assert_equal_i(2, fake_gatt_is_service_discovery_start_count());
// next job should be in progress
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), true);
// BP service was discovered
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
// start another job, should be pended
gatt_client_discovery_discover_range(connection, &range);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
// Since we are restarting discovery over the same range and discovered one
// service two events should be generated, one about the discovery complete
// and one invalidating the just discovered service
cl_assert_equal_i(2, fake_event_get_count());
prv_assert_event(&device, BTErrnoOK);
cl_assert_equal_i(3, fake_gatt_is_service_discovery_start_count());
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
gatt_client_discovery_discover_range(connection, &range_alt);
// BP service should have been discovered
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
// Only one event should have been pended since we were not rediscovering the
// same handle range
cl_assert_equal_i(1, fake_event_get_count());
prv_assert_event(&device, BTErrnoOK);
// Nothing discovered for final query
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
prv_assert_event(&device, BTErrnoOK);
cl_assert_equal_i(4, fake_gatt_is_service_discovery_start_count());
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
}
void test_gatt_client_discovery__partial_and_full_discovery_jobs_intermixed(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
GAPLEConnection *connection = gap_le_connection_by_device(&device);
// queue up a few jobs - note only one should be running at any time
ATTHandleRange range = {
.start = 0x1,
.end = 0x3000
};
for (int i = 0; i < 10; i++) {
gatt_client_discovery_discover_range(connection, &range);
}
// kick off a full discovery
cl_assert_equal_i(gatt_client_discovery_rediscover_all(&device), BTErrnoOK);
// Assert the previous process has been stopped:
cl_assert_equal_i(1, fake_gatt_is_service_discovery_stop_count());
// Expect "Database Changed" event:
prv_assert_event(&device, BTErrnoServiceDiscoveryDatabaseChanged);
prv_simulate_and_assert_discovery_of_one_service(&device);
// None of the batched up jobs should have run
cl_assert_equal_i(2, fake_gatt_is_service_discovery_start_count());
// nothing should still be running
cl_assert_equal_b(fake_gatt_is_service_discovery_running(), false);
}
// -------------------------------------------------------------------------------------------------
// Test vectors
static void prv_assert_blood_pressure_service(const GATTService *service) {
const Service *bp_service = fake_gatt_get_blood_pressure_service();
const uint16_t service_handle = bp_service->handle;
cl_assert_equal_i(service->att_handle, service_handle);
cl_assert_equal_b(uuid_equal(&service->uuid, &bp_service->uuid), true);
cl_assert_equal_i(service->num_att_handles_included_services, bp_service->num_included_services);
cl_assert_equal_i(service->num_characteristics, bp_service->num_characteristics);
const GATTCharacteristic *characteristic_one = service->characteristics;
const Characteristic *expected_characteristic1 = &bp_service->characteristics[0];
cl_assert_equal_i(characteristic_one->att_handle_offset, expected_characteristic1->handle - service_handle);
cl_assert_equal_i(characteristic_one->num_descriptors, expected_characteristic1->num_descriptors);
cl_assert_equal_i(characteristic_one->descriptors[0].att_handle_offset,
expected_characteristic1->descriptors[0].handle - service_handle);
cl_assert_equal_i(characteristic_one->properties, expected_characteristic1->properties);
cl_assert_equal_b(uuid_equal(&characteristic_one->uuid, &expected_characteristic1->uuid), true);
// Second characteristic is tacked right after the first one:
const GATTCharacteristic *characteristic_two =
(const GATTCharacteristic *) &characteristic_one->descriptors[1];
const Characteristic *expected_characteristic2 = &bp_service->characteristics[1];
cl_assert_equal_i(characteristic_two->att_handle_offset, expected_characteristic2->handle - service_handle);
cl_assert_equal_i(characteristic_two->num_descriptors, expected_characteristic2->num_descriptors);
cl_assert_equal_i(characteristic_two->descriptors[0].att_handle_offset,
expected_characteristic2->descriptors[0].handle - service_handle);
cl_assert_equal_i(characteristic_two->properties, expected_characteristic2->properties);
cl_assert_equal_b(uuid_equal(&characteristic_two->uuid, &expected_characteristic2->uuid), true);
}
void test_gatt_client_discovery__single_blood_pressure_service(void) {
BTDeviceInternal device = prv_connected_dummy_device(1);
// Start discovery:
cl_assert_equal_i(gatt_client_discovery_discover_all(&device), BTErrnoOK);
prv_simulate_and_assert_discovery_of_one_service(&device);
GAPLEConnection *connection = gap_le_connection_by_gatt_id(TEST_GATT_CONNECTION_ID);
// Expect one service, Blood Pressure:
cl_assert_equal_i(list_count(&connection->gatt_remote_services->node), 1);
const GATTService *service = connection->gatt_remote_services->service;
prv_assert_blood_pressure_service(service);
}
@@ -0,0 +1,942 @@
/*
* 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 <bluetooth/gatt.h>
#include "comm/ble/gatt_client_subscriptions.h"
#include "comm/ble/gap_le_connection.h"
#include "comm/ble/gap_le_task.h"
#include "comm/ble/gatt_service_changed.h"
#include "clar.h"
#include <btutil/bt_device.h>
#include <btutil/bt_uuid.h>
#include "FreeRTOS.h"
#include "semphr.h"
// Fakes
///////////////////////////////////////////////////////////
#include "fake_events.h"
#include "fake_pbl_malloc.h"
#include "fake_GATTAPI.h"
#include "fake_GATTAPI_test_vectors.h"
#include "fake_new_timer.h"
#include "fake_queue.h"
#include "fake_system_task.h"
#include "stubs_regular_timer.h"
static BTErrno s_write_descriptor_cccd_result;
static BLEDescriptor s_last_cccd_ref;
static uint16_t s_last_cccd_value;
BTErrno gatt_client_op_write_descriptor_cccd(BLEDescriptor cccd, const uint16_t *value) {
s_last_cccd_ref = cccd;
s_last_cccd_value = *value;
return s_write_descriptor_cccd_result;
}
// FIXME: PBL-23945
void fake_kernel_malloc_mark(void) { }
void fake_kernel_malloc_mark_assert_equal(void) { }
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_analytics.h"
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_driver_gatt.h"
#include "stubs_bt_lock.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_rand_ptr.h"
#include "stubs_tick.h"
void core_dump_reset(bool is_forced) {
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
uint16_t gaps_get_starting_att_handle(void) {
return 4;
}
// Helpers
///////////////////////////////////////////////////////////
extern void gatt_client_subscriptions_handle_server_notification(GAPLEConnection *connection,
uint16_t att_handle,
const uint8_t *att_value,
uint16_t att_length);
extern void gatt_client_subscriptions_handle_write_cccd_response(BLEDescriptor cccd,
BLEGATTError error);
extern uint16_t gatt_client_characteristic_get_handle_and_connection(
BLECharacteristic characteristic_ref,
GAPLEConnection **connection);
extern SemaphoreHandle_t gatt_client_subscription_get_semaphore(void);
extern void gatt_client_subscription_cleanup(void);
#define TEST_GATT_CONNECTION_ID (1234)
#define BOGUS_CHARACTERISTIC ((BLECharacteristic) 888)
#define BOGUS_ATT_HANDLE ((uint16_t) 0xffff)
static BTDeviceInternal s_device;
static GAPLEConnection *s_connection;
static uint16_t s_handle;
static BTDeviceInternal prv_dummy_device(uint8_t octet) {
BTDeviceAddress address = {
.octets = {
[0] = octet,
[1] = octet,
[2] = octet,
[3] = octet,
[4] = octet,
[5] = octet,
},
};
BTDevice device = bt_device_init_with_address(address, true /* is_random */);
return *(BTDeviceInternal *)(&device);
}
static BTDeviceInternal prv_connected_dummy_device(uint8_t octet) {
BTDeviceInternal device = prv_dummy_device(octet);
gap_le_connection_add(&device, NULL, true /* local_is_master */);
s_connection = gap_le_connection_by_device(&device);
s_connection->gatt_connection_id = TEST_GATT_CONNECTION_ID;
return device;
}
static void prv_assert_no_event(void) {
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_NULL_EVENT);
}
static void prv_assert_subscription_event(BLECharacteristic characteristic,
BLESubscription subscription_type,
BLEGATTError error, bool kernel, bool app) {
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_BLE_GATT_CLIENT_EVENT);
cl_assert_equal_i(event.bluetooth.le.gatt_client.subtype,
PebbleBLEGATTClientEventTypeCharacteristicSubscribe);
cl_assert_equal_i(event.bluetooth.le.gatt_client.subscription_type, subscription_type);
cl_assert_equal_i(event.bluetooth.le.gatt_client.object_ref, characteristic);
cl_assert_equal_i(event.bluetooth.le.gatt_client.gatt_error, error);
PebbleTaskBitset task_mask = ~0;
if (kernel) {
task_mask &= ~gap_le_pebble_task_bit_for_client(GAPLEClientKernel);
}
if (app) {
task_mask &= ~gap_le_pebble_task_bit_for_client(GAPLEClientApp);
}
cl_assert_equal_i(event.task_mask, task_mask);
}
static void prv_assert_notification_event_ext(BLECharacteristic characteristic,
const uint8_t *value, uint16_t assert_value_length,
bool kernel, bool app, bool should_consume) {
PebbleEvent event = fake_event_get_last();
cl_assert_equal_i(event.type, PEBBLE_BLE_GATT_CLIENT_EVENT);
cl_assert_equal_i(event.bluetooth.le.gatt_client.subtype,
PebbleBLEGATTClientEventTypeNotification);
PebbleTaskBitset task_mask = ~0;
if (kernel) {
task_mask &= ~gap_le_pebble_task_bit_for_client(GAPLEClientKernel);
}
if (app) {
task_mask &= ~gap_le_pebble_task_bit_for_client(GAPLEClientApp);
}
cl_assert_equal_i(event.task_mask, task_mask);
BLECharacteristic characteristic_out;
uint8_t *buffer = (uint8_t *) malloc(assert_value_length);
memset(buffer, 0, assert_value_length);
if (kernel) {
GATTBufferedNotificationHeader header = {};
gatt_client_subscriptions_get_notification_header(GAPLEClientKernel, &header);
cl_assert_equal_i(header.value_length, assert_value_length);
cl_assert_equal_i(header.characteristic, characteristic);
if (should_consume) {
uint16_t value_length = assert_value_length;
gatt_client_subscriptions_consume_notification(&characteristic_out, buffer, &value_length,
GAPLEClientKernel, NULL);
cl_assert_equal_i(memcmp(buffer, value, value_length), 0);
cl_assert_equal_i(characteristic_out, characteristic);
}
}
memset(buffer, 0, assert_value_length);
if (app) {
GATTBufferedNotificationHeader header = {};
gatt_client_subscriptions_get_notification_header(GAPLEClientApp, &header);
cl_assert_equal_i(header.value_length, assert_value_length);
cl_assert_equal_i(header.characteristic, characteristic);
if (should_consume) {
uint16_t value_length = assert_value_length;
gatt_client_subscriptions_consume_notification(&characteristic_out, buffer, &value_length,
GAPLEClientApp, NULL);
cl_assert_equal_i(memcmp(buffer, value, value_length), 0);
cl_assert_equal_i(characteristic_out, characteristic);
}
}
free(buffer);
}
static void prv_assert_notification_event(BLECharacteristic characteristic,
const uint8_t *value, uint16_t assert_value_length,
bool kernel, bool app) {
prv_assert_notification_event_ext(characteristic, value, assert_value_length, kernel, app,
true /* should_consume */);
}
static void prv_simulate_and_assert_discovery_of_one_service(const BTDeviceInternal *device) {
// Simulate discovery of Blood Pressure service:
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
// Simulate discovery of random 128-bit service:
fake_gatt_put_discovery_indication_random_128bit_uuid_service(TEST_GATT_CONNECTION_ID);
fake_gatt_put_discovery_complete_event(GATT_SERVICE_DISCOVERY_STATUS_SUCCESS,
TEST_GATT_CONNECTION_ID);
}
static BLECharacteristic prv_get_indicatable_characteristic(void) {
// The Blood Pressure Service UUID:
Uuid service_uuid = bt_uuid_expand_16bit(0x1810);
BLEService service;
uint8_t num_copied = gatt_client_copy_service_refs_matching_uuid(&s_device, &service, 1,
&service_uuid);
cl_assert_equal_i(num_copied, 1);
// UUID for indicatable Pressure Measurement characteristic:
Uuid characteristic_uuid = bt_uuid_expand_16bit(0x2a35);
BLECharacteristic characteristic;
num_copied = gatt_client_service_get_characteristics_matching_uuids(service, &characteristic,
&characteristic_uuid, 1);
cl_assert_equal_i(num_copied, 1);
GAPLEConnection *connection;
s_handle = gatt_client_characteristic_get_handle_and_connection(characteristic, &connection);
return characteristic;
}
static void prv_confirm_cccd_write(BLEGATTError error) {
gatt_client_subscriptions_handle_write_cccd_response(s_last_cccd_ref, error);
}
// Tests
///////////////////////////////////////////////////////////
void test_gatt_client_subscriptions__initialize(void) {
s_write_descriptor_cccd_result = BTErrnoOK;
fake_pbl_malloc_clear_tracking();
fake_event_init();
gap_le_connection_init();
gatt_client_subscription_boot();
// Prepare connected device with Blood Pressure GATT service discovered:
s_device = prv_connected_dummy_device(1);
cl_assert_equal_i(gatt_client_discovery_discover_all(&s_device), BTErrnoOK);
prv_simulate_and_assert_discovery_of_one_service(&s_device);
fake_event_clear_last();
}
void test_gatt_client_subscriptions__cleanup(void) {
for (GAPLEClient c = 0; c < GAPLEClientNum; ++c) {
gatt_client_subscriptions_cleanup_by_client(c);
}
gap_le_connection_deinit();
gatt_client_subscription_cleanup();
fake_pbl_malloc_check_net_allocs();
fake_pbl_malloc_clear_tracking();
}
// -------------------------------------------------------------------------------------------------
// gatt_client_subscriptions_subscribe
void test_gatt_client_subscriptions__subscribe_invalid_characteristic(void) {
fake_kernel_malloc_mark();
BTErrno e = gatt_client_subscriptions_subscribe(BOGUS_CHARACTERISTIC,
BLESubscriptionAny, GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__subscribe_no_cccd(void) {
// The random 128-bit Service UUID:
Uuid service_uuid = UuidMake(0xF7, 0x68, 0x09, 0x5B, 0x1B, 0xFA, 0x4F, 0x63,
0x97, 0xEE, 0xFD, 0xED, 0xAC, 0x66, 0xF9, 0xB0);
BLEService service;
uint8_t num_copied = gatt_client_copy_service_refs_matching_uuid(&s_device, &service, 1,
&service_uuid);
cl_assert_equal_i(num_copied, 1);
// UUID for Characteristic that has no CCCD:
Uuid characteristic_uuid = UuidMake(0xF7, 0x68, 0x09, 0x5B, 0x1B, 0xFA, 0x4F, 0x63,
0x97, 0xEE, 0xFD, 0xED, 0xAC, 0x66, 0xF9, 0xB1);
BLECharacteristic characteristic;
num_copied = gatt_client_service_get_characteristics_matching_uuids(service, &characteristic,
&characteristic_uuid, 1);
cl_assert_equal_i(num_copied, 1);
fake_kernel_malloc_mark();
// Try to subscribe to the non-subscribe-able characteristic:
BTErrno e = gatt_client_subscriptions_subscribe(characteristic,
BLESubscriptionAny, GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__subscribe_unsupported_subscription_type(void) {
BTErrno e;
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
fake_kernel_malloc_mark();
// Try to subscribe for notifications to the indicatable (but not notify-able) characteristic:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionNotifications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoInvalidParameter);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
// Try to subscribe for indications to the indicatable characteristic:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
}
void test_gatt_client_subscriptions__subscribe_already_subscribed(void) {
BTErrno e;
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
fake_event_clear_last();
fake_kernel_malloc_mark();
// Subscribe again:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoInvalidState);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__unsubscribe_pending_subscription(void) {
BTErrno e;
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
fake_event_clear_last();
fake_kernel_malloc_mark();
// Un-subscribe, while subscribing process is still pending:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionNone, GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoInvalidState);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__subscribe_oom_for_subscription_allocation(void) {
fake_kernel_malloc_mark();
fake_malloc_set_largest_free_block(sizeof(GATTClientSubscriptionNode) - 1);
BTErrno e = gatt_client_subscriptions_subscribe(prv_get_indicatable_characteristic(),
BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoNotEnoughResources);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__subscribe_oom_for_buffer_allocation(void) {
fake_kernel_malloc_mark();
fake_malloc_set_largest_free_block(sizeof(GATTClientSubscriptionNode));
BTErrno e = gatt_client_subscriptions_subscribe(prv_get_indicatable_characteristic(),
BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoNotEnoughResources);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__subscribe_cccd_write_error(void) {
s_write_descriptor_cccd_result = -1;
fake_kernel_malloc_mark();
// Try to subscribe for indications to the indicatable characteristic:
BTErrno e = gatt_client_subscriptions_subscribe(prv_get_indicatable_characteristic(),
BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, -1);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__unsubscribe_no_clients_subscribed(void) {
fake_kernel_malloc_mark();
BTErrno e;
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionNone, GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoInvalidState);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__unsubscribe_not_subscribed_but_other_client_is(void) {
BTErrno e;
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications, GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
fake_kernel_malloc_mark();
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionNone, GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoInvalidState);
fake_kernel_malloc_mark_assert_equal();
prv_assert_no_event();
}
void test_gatt_client_subscriptions__subscribe_first_subscriber(void) {
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
cl_assert_equal_i(s_last_cccd_value, BLESubscriptionIndications);
prv_confirm_cccd_write(BLEGATTErrorSuccess);
prv_assert_subscription_event(characteristic, BLESubscriptionIndications, BLEGATTErrorSuccess,
true /* kernel */, false /* app */);
}
void test_gatt_client_subscriptions__subscribe_not_first_subscriber(void) {
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
// Subscribe kernel:
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
cl_assert_equal_i(s_last_cccd_value, BLESubscriptionIndications);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
// Subscribe app:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
// App should get event immediately:
prv_assert_subscription_event(characteristic, BLESubscriptionIndications, BLEGATTErrorSuccess,
false /* kernel */, true /* app */);
}
void test_gatt_client_subscriptions__two_subscribers_before_cccd_write_response(void) {
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
// Subscribe kernel:
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
cl_assert_equal_i(s_last_cccd_value, BLESubscriptionIndications);
s_last_cccd_value = ~0;
// Subscribe app:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Should not be written again, because BLESubscriptionIndications was already written:
cl_assert_equal_i(s_last_cccd_value, (uint16_t) ~0);
fake_event_clear_last();
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
// Expect one event for both Kernel + App:
prv_assert_subscription_event(characteristic, BLESubscriptionIndications, BLEGATTErrorSuccess,
true /* kernel */, true /* app */);
}
void test_gatt_client_subscriptions__unsubscribe_last_subscriber(void) {
BTErrno e;
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
fake_kernel_malloc_mark();
// Subscribe kernel:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
// Unsubscribe kernel:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionNone, GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
// Kernel should get event immediately:
prv_assert_subscription_event(characteristic, BLESubscriptionNone, BLEGATTErrorSuccess,
true /* kernel */, false /* app */);
// CCCD value should be 0 now:
cl_assert_equal_i(s_last_cccd_value, 0);
// Verify everything is cleaned up:
fake_kernel_malloc_mark_assert_equal();
}
void test_gatt_client_subscriptions__unsubscribe_not_last_subscriber(void) {
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
// Subscribe kernel:
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
cl_assert_equal_i(s_last_cccd_value, BLESubscriptionIndications);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
// Subscribe app:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
s_last_cccd_value = ~0;
// Unsubscribe kernel:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionNone, GAPLEClientKernel);
// Kernel should get event immediately:
prv_assert_subscription_event(characteristic, BLESubscriptionNone, BLEGATTErrorSuccess,
true /* kernel */, false /* app */);
// CCCD value should not be written, because app is still subscribed:
cl_assert_equal_i(s_last_cccd_value, (uint16_t) ~0);
}
void test_gatt_client_subscriptions__subscribe_failed_cccd_write(void) {
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
fake_kernel_malloc_mark();
// Subscribe kernel:
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
cl_assert_equal_i(s_last_cccd_value, BLESubscriptionIndications);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorInvalidHandle);
// Expect kernel event with 'BLESubscriptionNone' type and error bubbled up:
prv_assert_subscription_event(characteristic, BLESubscriptionNone, BLEGATTErrorInvalidHandle,
true /* kernel */, false /* app */);
// Verify everything is cleaned up:
fake_kernel_malloc_mark_assert_equal();
}
// -------------------------------------------------------------------------------------------------
// gatt_client_subscriptions_handle_server_notification &
// gatt_client_subscriptions_consume_notification
void test_gatt_client_subscriptions__notification_but_no_subscribers(void) {
const uint8_t value = 0xAA;
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
&value, sizeof(value));
prv_assert_no_event();
}
void test_gatt_client_subscriptions__cccd_write_confirmation_but_no_subscription(void) {
s_last_cccd_ref = 1;
prv_confirm_cccd_write(BLEGATTErrorSuccess);
// This used to cause a crash:
// https://pebbletechnology.atlassian.net/browse/PBL-23455
}
void test_gatt_client_subscriptions__notification_single_subscriber(void) {
// Subscribe app:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
// Nothing to be read before getting the notification:
bool has_notification = gatt_client_subscriptions_get_notification_header(GAPLEClientApp,
NULL);
cl_assert_equal_b(has_notification, false);
const uint8_t value[] = {0xAA, 0xBB, 0xCC};
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, sizeof(value));
prv_assert_notification_event(characteristic, value, sizeof(value),
false /* kernel */, true /* app */);
// Nothing to be read after "consuming" it:
has_notification = gatt_client_subscriptions_get_notification_header(GAPLEClientApp, NULL);
cl_assert_equal_b(has_notification, false);
}
void test_gatt_client_subscriptions__zero_length_notification(void) {
// Subscribe app:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle, NULL, 0);
prv_assert_notification_event(characteristic, NULL, 0, false /* kernel */, true /* app */);
// Nothing to be read after "consuming" it:
bool has_notification = gatt_client_subscriptions_get_notification_header(GAPLEClientApp, NULL);
cl_assert_equal_b(has_notification, false);
}
void test_gatt_client_subscriptions__notification_app_and_kernel_subscribers(void) {
// Subscribe app:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Subscribe kernel:
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
fake_event_reset_count();
const uint8_t value[] = {0xAA, 0xBB, 0xCC};
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, sizeof(value));
cl_assert_equal_i(fake_event_get_count(), 1);
// Send another notification, before reading out the previous one:
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, sizeof(value));
// Only one event should have been put on the queue:
cl_assert_equal_i(fake_event_get_count(), 1);
// Assert 2 events can be read out:
prv_assert_notification_event(characteristic, value, sizeof(value),
true /* kernel */, true /* app */);
prv_assert_notification_event(characteristic, value, sizeof(value),
true /* kernel */, true /* app */);
// Send the 3rd notification:
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, sizeof(value));
cl_assert_equal_i(fake_event_get_count(), 2);
prv_assert_notification_event(characteristic, value, sizeof(value),
true /* kernel */, true /* app */);
}
static TickType_t prv_taking_too_long_to_consume_yield_cb(QueueHandle_t queue) {
return milliseconds_to_ticks(1000);
}
static TickType_t prv_consume_in_time_yield_cb(QueueHandle_t queue) {
// Consume while BT task is waiting for buffer to be freed up:
BLECharacteristic characteristic_out;
uint8_t *value_out = (uint8_t *) malloc(GATT_CLIENT_SUBSCRIPTIONS_BUFFER_SIZE);
uint16_t value_length = GATT_CLIENT_SUBSCRIPTIONS_BUFFER_SIZE;
gatt_client_subscriptions_consume_notification(&characteristic_out, value_out, &value_length,
GAPLEClientApp, NULL);
free(value_out);
return milliseconds_to_ticks(5);
}
void test_gatt_client_subscriptions__notification_buffer_full(void) {
// Subscribe app:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
// We had a bug at some point where the header size was not taken into account correctly, take
// a value that won't fit by 1 byte, so this bug does not happen again:
const size_t too_big = GATT_CLIENT_SUBSCRIPTIONS_BUFFER_SIZE -
sizeof(GATTBufferedNotificationHeader) + 1;
uint8_t *value = (uint8_t *) malloc(too_big);
memset(value, 0x55, too_big);
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, too_big);
prv_assert_no_event();
// Receive a GATT notification that is supposed to fill up the buffer entirely:
const size_t fill_entirely_size = (GATT_CLIENT_SUBSCRIPTIONS_BUFFER_SIZE -
sizeof(GATTBufferedNotificationHeader));
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, fill_entirely_size);
prv_assert_notification_event_ext(characteristic, value, fill_entirely_size,
false /* kernel */, true /* app */, false /* should_consume */);
// Receive another GATT notification. Won't fit until consumed. Consuming is taking to long:
fake_queue_set_yield_callback(gatt_client_subscription_get_semaphore(),
prv_taking_too_long_to_consume_yield_cb);
fake_event_clear_last();
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, 1 /* one byte */);
// Data will be dropped, no event :(
prv_assert_no_event();
// Receive another GATT notification. Won't fit until consumed.
// Consuming is happening before the timeout hits (in the yield callback):
fake_queue_set_yield_callback(gatt_client_subscription_get_semaphore(),
prv_consume_in_time_yield_cb);
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, 1 /* one byte */);
prv_assert_notification_event(characteristic, value, 1 /* one byte */,
false /* kernel */, true /* app */);
free(value);
}
void test_gatt_client_subscriptions__consume_but_too_small_buffer(void) {
// Subscribe app:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
const uint8_t value[] = {0xAA, 0xBB, 0xCC};
gatt_client_subscriptions_handle_server_notification(s_connection, s_handle,
value, sizeof(value));
BLECharacteristic handle_out = ~0;
uint8_t out[1];
uint16_t value_length = sizeof(out);
bool has_more = true;
const uint16_t next_length =
gatt_client_subscriptions_consume_notification(&handle_out, out, &value_length,
GAPLEClientApp, &has_more);
cl_assert_equal_i(handle_out, BLE_CHARACTERISTIC_INVALID);
cl_assert_equal_i(value_length, 0);
// Notification will be eaten, regardless of whether it was copied:
cl_assert_equal_b(has_more, false);
cl_assert_equal_i(next_length, 0);
}
void test_gatt_client_subscriptions__consume_but_nothing_in_buffer(void) {
// Subscribe app:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
// Simulate getting confirmation from remote:
prv_confirm_cccd_write(BLEGATTErrorSuccess);
fake_event_clear_last();
BLECharacteristic handle_out = ~0;
uint8_t out[1];
uint16_t value_length = sizeof(out);
bool has_more = true;
const uint16_t next_length =
gatt_client_subscriptions_consume_notification(&handle_out, out, &value_length,
GAPLEClientApp, &has_more);
cl_assert_equal_i(handle_out, BLE_CHARACTERISTIC_INVALID);
cl_assert_equal_i(value_length, 0);
cl_assert_equal_b(has_more, false);
cl_assert_equal_i(next_length, 0);
}
void test_gatt_client_subscriptions__consume_but_buffer_client_buffer_null(void) {
bool has_more = true;
BLECharacteristic handle_out = ~0;
uint8_t out[1];
uint16_t value_length = sizeof(out);
const uint16_t next_length =
gatt_client_subscriptions_consume_notification(&handle_out, out, &value_length,
GAPLEClientApp, &has_more);
cl_assert_equal_i(next_length, 0);
cl_assert_equal_b(has_more, false);
}
void test_gatt_client_subscriptions__notification_consume_without_notification(void) {
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
uint16_t value_length;
GATTBufferedNotificationHeader header = {
.characteristic = BLE_CHARACTERISTIC_INVALID,
.value_length = 0,
};
bool has_more = gatt_client_subscriptions_get_notification_header(GAPLEClientKernel,
&header);
cl_assert_equal_i(header.value_length, 0);
cl_assert_equal_i(header.characteristic, BLE_CHARACTERISTIC_INVALID);
cl_assert_equal_b(has_more, false);
BLECharacteristic characteristic_out = ~0;
uint8_t value = 0xff;
value_length = sizeof(value);
gatt_client_subscriptions_consume_notification(&characteristic_out, &value, &value_length,
GAPLEClientKernel, &has_more);
// Expect untouched:
cl_assert_equal_i(characteristic_out, ~0);
cl_assert_equal_i(value, 0xff);
cl_assert_equal_b(has_more, false);
}
// -------------------------------------------------------------------------------------------------
// gatt_client_subscriptions_cleanup_by_client
void test_gatt_client_subscriptions__cleanup_by_client(void) {
fake_kernel_malloc_mark();
// Subscribe app:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
gatt_client_subscriptions_cleanup_by_client(GAPLEClientApp);
prv_assert_no_event();
fake_kernel_malloc_mark_assert_equal();
}
// -------------------------------------------------------------------------------------------------
// gatt_client_subscriptions_cleanup_by_connection
bool gatt_client_get_event_pending_state(GAPLEClient);
static void prv_pend_events_to_kernel_and_app(void) {
// fake pend an event to the kernel
gatt_client_subscriptions_reschedule(GAPLEClientKernel);
cl_assert_equal_b(gatt_client_get_event_pending_state(GAPLEClientKernel), true);
fake_event_clear_last();
// fake pend an event to the app
gatt_client_subscriptions_reschedule(GAPLEClientApp);
cl_assert_equal_b(gatt_client_get_event_pending_state(GAPLEClientApp), true);
fake_event_clear_last();
}
static void prv_assert_no_pending_events_to_kernel_and_app(void) {
cl_assert_equal_b(gatt_client_get_event_pending_state(GAPLEClientKernel), false);
cl_assert_equal_b(gatt_client_get_event_pending_state(GAPLEClientApp), false);
}
void test_gatt_client_subscriptions__cleanup_by_connection(void) {
fake_kernel_malloc_mark();
// Subscribe app and kernel:
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
prv_pend_events_to_kernel_and_app();
gatt_client_subscriptions_cleanup_by_connection(s_connection, false /* should_unsubscribe */);
prv_assert_no_event();
// there should be no more subscriptions
cl_assert(s_connection->gatt_subscriptions == NULL);
prv_assert_no_pending_events_to_kernel_and_app();
fake_kernel_malloc_mark_assert_equal();
}
extern void gatt_client_subscription_cleanup_by_att_handle_range(
struct GAPLEConnection *connection, ATTHandleRange *range);
void test_gatt_client_subscriptions__cleanup_by_att_handle_range(void) {
fake_kernel_malloc_mark();
BLECharacteristic characteristic = prv_get_indicatable_characteristic();
BTErrno e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientApp);
cl_assert_equal_i(e, BTErrnoOK);
e = gatt_client_subscriptions_subscribe(characteristic, BLESubscriptionIndications,
GAPLEClientKernel);
cl_assert_equal_i(e, BTErrnoOK);
cl_assert(s_connection->gatt_subscriptions != NULL);
ATTHandleRange range;
fake_gatt_get_bp_att_handle_range(&range.start, &range.end);
ATTHandleRange bogus_range = {
.start = range.end + 1,
.end = range.end + 5
};
prv_pend_events_to_kernel_and_app();
// should have no effect since service is not in this range
gatt_client_subscription_cleanup_by_att_handle_range(s_connection, &bogus_range);
prv_assert_no_event();
cl_assert(s_connection->gatt_subscriptions != NULL);
// should actually remove everything
gatt_client_subscription_cleanup_by_att_handle_range(s_connection, &range);
prv_assert_no_event();
// there should be no more subscriptions
cl_assert(s_connection->gatt_subscriptions == NULL);
prv_assert_no_pending_events_to_kernel_and_app();
fake_kernel_malloc_mark_assert_equal();
}
// -------------------------------------------------------------------------------------------------
// TODO: Write tests that exercise applib/bluetooth/ble_client.c
@@ -0,0 +1,236 @@
/*
* 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 "comm/ble/gatt_service_changed.h"
#include "comm/ble/gap_le_connection.h"
#include "kernel/events.h"
#include "clar.h"
#include <btutil/bt_device.h>
// Fakes
///////////////////////////////////////////////////////////
#include "fake_GAPAPI.h"
#include "fake_GATTAPI.h"
#include "fake_GATTAPI_test_vectors.h"
#include "fake_pbl_malloc.h"
#include "fake_new_timer.h"
#include "fake_rtc.h"
#include "fake_system_task.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_driver_gatt.h"
#include "stubs_bt_lock.h"
#include "stubs_events.h"
#include "stubs_gatt_client_subscriptions.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_prompt.h"
#include "stubs_rand_ptr.h"
#include "stubs_regular_timer.h"
extern bool prv_contains_service_changed_characteristic(
GAPLEConnection *connection,
const GATT_Service_Discovery_Indication_Data_t *event);
void core_dump_reset(bool is_forced) {
}
static GAPLEConnection s_connection;
GAPLEConnection *gap_le_connection_by_device(const BTDeviceInternal *addr) {
return &s_connection;
}
GAPLEConnection *gap_le_connection_by_addr(const BTDeviceAddress *addr) {
return &s_connection;
}
GAPLEConnection *gap_le_connection_by_gatt_id(unsigned int connection_id) {
return &s_connection;
}
bool gap_le_connection_is_valid(const GAPLEConnection *conn) {
return true;
}
GAPLEConnection *gap_le_connection_any(void) {
return NULL;
}
uint16_t gaps_get_starting_att_handle(void) {
return 4;
}
GAPLEConnection *gatt_client_characteristic_get_connection(BLECharacteristic characteristic_ref) {
return NULL;
}
BLEService gatt_client_att_handle_get_service(
GAPLEConnection *connection, uint16_t att_handle, const GATTServiceNode **service_node_out) {
return 0;
}
uint8_t gatt_client_copy_service_refs_by_discovery_generation(
const BTDeviceInternal *device, BLEService services_out[],
uint8_t num_services, uint8_t discovery_gen) { return 0;}
void gatt_client_service_get_all_characteristics_and_descriptors(
GAPLEConnection *connection, GATTService *service,
BLECharacteristic *characteristic_hdls_out,
BLEDescriptor *descriptor_hdls_out) { }
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
// FIXME: PBL-23945
void fake_kernel_malloc_mark(void) { }
void fake_kernel_malloc_mark_assert_equal(void) { }
// Helpers
///////////////////////////////////////////////////////////
#define TEST_GATT_CONNECTION_ID (1234)
#define TEST_BT_STACK_ID (1)
typedef enum {
Unknown,
Handled,
Unhandled,
} HandleResult;
static HandleResult s_last_handle_discovery_result;
static void prv_bluetopia_service_discovery_cb(unsigned int stack_id,
GATT_Service_Discovery_Event_Data_t *event,
unsigned long callback_param) {
cl_assert_equal_i(stack_id, TEST_BT_STACK_ID);
if (event->Event_Data_Type == etGATT_Service_Discovery_Indication) {
const GATT_Service_Discovery_Indication_Data_t *indication =
event->Event_Data.GATT_Service_Discovery_Indication_Data;
cl_assert_equal_i(s_connection.gatt_connection_id, TEST_GATT_CONNECTION_ID);
cl_assert_equal_i(indication->ConnectionID, TEST_GATT_CONNECTION_ID);
s_last_handle_discovery_result =
prv_contains_service_changed_characteristic(&s_connection, indication) ? Handled : Unhandled;
}
}
// Tests
///////////////////////////////////////////////////////////
void test_gatt_service_changed_client__initialize(void) {
s_last_handle_discovery_result = Unknown;
fake_gatt_init();
s_connection = (GAPLEConnection) {
.gatt_connection_id = TEST_GATT_CONNECTION_ID,
.gatt_service_changed_att_handle = 0,
};
GATT_Start_Service_Discovery_Handle_Range(TEST_BT_STACK_ID, TEST_GATT_CONNECTION_ID, NULL, 0, NULL,
prv_bluetopia_service_discovery_cb, 0);
}
void test_gatt_service_changed_client__cleanup(void) {
}
// Discovery
///////////////////////////////////////////////////////////
void test_gatt_service_changed_client__handle_non_gatt_profile_service(void) {
fake_gatt_put_discovery_indication_blood_pressure_service(TEST_GATT_CONNECTION_ID);
cl_assert_equal_i(s_last_handle_discovery_result, Unhandled);
}
void test_gatt_service_changed_client__handle_gatt_profile_service(void) {
fake_gatt_put_discovery_indication_gatt_profile_service(TEST_GATT_CONNECTION_ID,
true /* has_service_changed_characteristic */);
cl_assert_equal_i(s_last_handle_discovery_result, Handled);
// Verify the CCCD of the Service Changed characteristic has been written:
cl_assert_equal_i(fake_gatt_write_last_written_handle(),
fake_gatt_gatt_profile_service_service_changed_cccd_att_handle());
// Simulate getting a Write Response confirmation for the written CCCD:
fake_gatt_put_write_response_for_last_write();
// Today we don't really do anything upon getting the confirmation
}
void test_gatt_service_changed_client__handle_gatt_profile_service_missing_service_changed(void) {
fake_gatt_put_discovery_indication_gatt_profile_service(TEST_GATT_CONNECTION_ID,
false /* has_service_changed_characteristic */);
cl_assert_equal_i(s_last_handle_discovery_result, Handled);
}
// Characteristic Value Indications
///////////////////////////////////////////////////////////
void test_gatt_service_changed_client__handle_indication_non_service_changed(void) {
fake_gatt_put_discovery_indication_gatt_profile_service(TEST_GATT_CONNECTION_ID,
true /* has_service_changed_characteristic */);
const uint8_t value;
const bool handled = gatt_service_changed_client_handle_indication(&s_connection, 0xfffe,
&value, sizeof(value));
cl_assert_equal_b(handled, false);
}
void test_gatt_service_changed_client__handle_indication_service_changed(void) {
fake_gatt_put_discovery_indication_gatt_profile_service(TEST_GATT_CONNECTION_ID,
true /* has_service_changed_characteristic */);
const uint16_t att_handle = fake_gatt_gatt_profile_service_service_changed_att_handle();
fake_kernel_malloc_mark();
const int start_count_before_indication = fake_gatt_is_service_discovery_start_count();
const uint16_t handle_range[2] = {
[0] = 0x1,
[1] = 0xfffe,
};
const bool handled = gatt_service_changed_client_handle_indication(&s_connection, att_handle,
(const uint8_t *) handle_range, sizeof(uint16_t) * 2);
// Re-discovery is trigger on KernelBG:
fake_system_task_callbacks_invoke_pending();
// The KernelBG trip uses kernel_malloc, make sure it's cleaning up properly:
fake_kernel_malloc_mark_assert_equal();
cl_assert_equal_b(handled, true);
// Expect service discovery to be started once more:
cl_assert_equal_i(start_count_before_indication + 1,
fake_gatt_is_service_discovery_start_count());
}
void test_gatt_service_changed_client__handle_indication_service_changed_malformatted(void) {
fake_gatt_put_discovery_indication_gatt_profile_service(TEST_GATT_CONNECTION_ID,
true /* has_service_changed_characteristic */);
const uint16_t att_handle = fake_gatt_gatt_profile_service_service_changed_att_handle();
const uint16_t handle_range[1] = {
[0] = 0x1,
};
const bool handled = gatt_service_changed_client_handle_indication(&s_connection, att_handle,
(const uint8_t *) handle_range, sizeof(uint16_t) * 1);
cl_assert_equal_b(handled, true);
}
@@ -0,0 +1,213 @@
/*
* 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 "comm/ble/gatt_service_changed.h"
#include "comm/ble/gap_le_connection.h"
#include "bluetopia_interface.h"
#include "kernel/events.h"
#include "clar.h"
#include <btutil/bt_device.h>
extern void gatt_service_changed_server_init(void);
// Fakes
///////////////////////////////////////////////////////////
#include "fake_GAPAPI.h"
#include "fake_GATTAPI.h"
#include "fake_GATTAPI_test_vectors.h"
#include "fake_pbl_malloc.h"
#include "fake_new_timer.h"
#include "fake_rtc.h"
#include "fake_system_task.h"
// Stubs
///////////////////////////////////////////////////////////
#include "stubs_bluetopia_interface.h"
#include "stubs_bt_driver_gatt.h"
#include "stubs_bt_driver_gatt_client_discovery.h"
#include "stubs_bt_lock.h"
#include "stubs_events.h"
#include "stubs_gatt_client_subscriptions.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_prompt.h"
#include "stubs_rand_ptr.h"
#include "stubs_regular_timer.h"
uint16_t gaps_get_starting_att_handle(void) {
return 4;
}
BLEService gatt_client_att_handle_get_service(
GAPLEConnection *connection, uint16_t att_handle, const GATTServiceNode **service_node_out) {
return 0;
}
uint8_t gatt_client_copy_service_refs_by_discovery_generation(
const BTDeviceInternal *device, BLEService services_out[],
uint8_t num_services, uint8_t discovery_gen) {
return 0;
}
void gatt_client_service_get_all_characteristics_and_descriptors(
GAPLEConnection *connection, GATTService *service,
BLECharacteristic *characteristic_hdls_out,
BLEDescriptor *descriptor_hdls_out) {
}
void launcher_task_add_callback(void (*callback)(void *data), void *data) {
callback(data);
}
// Helpers
///////////////////////////////////////////////////////////
static const BTDeviceInternal s_device = {
.address = {
.octets = {
1, 2, 3, 4, 5, 6,
},
},
};
static uint32_t s_connection_id = 1;
static GAPLEConnection *s_connection;
static void prv_cccd_write(bool is_subscribing) {
GattServerSubscribeEvent event = {
.connection_id = s_connection_id,
.dev_address = s_device.address,
.is_subscribing = is_subscribing,
};
bt_driver_cb_gatt_service_changed_server_subscribe(&event);
}
static void prv_process_pending_callbacks(GAPLEConnection *connection) {
if (connection && connection->gatt_service_changed_indication_timer) {
stub_new_timer_fire(connection->gatt_service_changed_indication_timer);
}
fake_system_task_callbacks_invoke_pending();
}
#define prv_expect_service_changed_indication_api_call_count(expected_count) \
{ \
prv_process_pending_callbacks(s_connection); \
cl_assert_equal_i(fake_gatt_get_service_changed_indication_count(), expected_count); \
}
// Tests
///////////////////////////////////////////////////////////
void test_gatt_service_changed_server__initialize(void) {
gatt_service_changed_server_init();
fake_gatt_init();
gap_le_connection_init();
gap_le_connection_add(&s_device, NULL, false /* local_is_master */);
s_connection = gap_le_connection_by_device(&s_device);
cl_assert(s_connection);
s_connection->gatt_connection_id = s_connection_id;
}
void test_gatt_service_changed_server__cleanup(void) {
if (s_connection) {
gap_le_connection_remove(&s_device);
s_connection = NULL;
}
gap_le_connection_deinit();
stub_new_timer_cleanup();
}
void test_gatt_service_changed_server__unsubscribe(void) {
prv_cccd_write(false /* is_subscribing */);
prv_expect_service_changed_indication_api_call_count(0);
}
void test_gatt_service_changed_server__subscribe_event_but_no_connection(void) {
gap_le_connection_remove(&s_device);
s_connection = NULL;
prv_cccd_write(true /* is_subscribing */);
prv_expect_service_changed_indication_api_call_count(0);
}
void test_gatt_service_changed_server__subscribe_fw_not_updated(void) {
prv_cccd_write(true /* is_subscribing */);
prv_expect_service_changed_indication_api_call_count(0);
}
void test_gatt_service_changed_server__resubscribe_indication_already_pending(void) {
gatt_service_changed_server_handle_fw_update();
prv_cccd_write(true /* is_subscribing */);
prv_cccd_write(true /* is_subscribing */);
prv_expect_service_changed_indication_api_call_count(1);
}
void test_gatt_service_changed_server__reconnect_resubscribe_stop_sending_after_n_times(void) {
gatt_service_changed_server_handle_fw_update();
gap_le_connection_remove(&s_device);
s_connection = NULL;
static const int max_times = 5;
for (int i = 0; i < max_times + 1; ++i) {
gap_le_connection_add(&s_device, NULL, false /* local_is_master */);
s_connection = gap_le_connection_by_device(&s_device);
cl_assert(s_connection);
s_connection->gatt_connection_id = s_connection_id;
prv_cccd_write(true /* is_subscribing */);
if (i < max_times) {
prv_expect_service_changed_indication_api_call_count(i + 1);
} else {
prv_expect_service_changed_indication_api_call_count(max_times);
}
gap_le_connection_remove(&s_device);
s_connection = NULL;
}
}
void test_gatt_service_changed_server__disconnect_during_delay(void) {
gatt_service_changed_server_handle_fw_update();
prv_cccd_write(true /* is_subscribing */);
// Grab the timer ID:
TimerID t = s_connection->gatt_service_changed_indication_timer;
s_connection->gatt_service_changed_indication_timer = TIMER_INVALID_ID;
// Simulate disconnection:
gap_le_connection_remove(&s_device);
s_connection = NULL;
// Timer fires:
stub_new_timer_fire(t);
prv_expect_service_changed_indication_api_call_count(0);
}
+297
View File
@@ -0,0 +1,297 @@
/*
* 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 "clar.h"
#include "comm/ble/gap_le_advert.h"
#include "comm/ble/gap_le_task.h"
#include "comm/ble/gatt_client_subscriptions.h"
#include "comm/ble/kernel_le_client/kernel_le_client.h"
#include "comm/ble/kernel_le_client/test/test_definition.h"
#include "kernel/events.h"
#include "util/size.h"
// Stubs
////////////////////////////////////////////////////////////////////////////////////////////////////
#include "fake_system_task.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_rand_ptr.h"
#include "stubs_rtc.h"
void ams_create(void) {
}
void ams_destroy(void) {
}
void ancs_create(void) {
}
void ancs_destroy(void) {
}
void app_launch_handle_disconnection(void) {
}
BTBondingID bt_persistent_storage_get_ble_ancs_bonding(void) {
return 1;
}
bool bt_persistent_storage_is_ble_ancs_bonding(BTBondingID bonding) {
return true;
}
void gap_le_advert_unschedule_job_types(GAPLEAdvertisingJobTag *tag_types, size_t num_types) {
}
void gap_le_connect_cancel_all(GAPLEClient client) {
}
BTErrno gap_le_connect_cancel_by_bonding(BTBondingID bonding_id, GAPLEClient client) {
return BTErrnoOK;
}
BTErrno gap_le_connect_connect_by_bonding(BTBondingID bonding_id, bool auto_reconnect,
bool is_pairing_required, GAPLEClient client) {
return BTErrnoOK;
}
void gap_le_slave_reconnect_start(void) {
}
void gap_le_slave_reconnect_stop(void) {
}
BTErrno gatt_client_discovery_discover_all(const BTDeviceInternal *device) {
return BTErrnoOK;
}
uint16_t gatt_client_subscriptions_consume_notification(BLECharacteristic *characteristic_ref_out,
uint8_t *value_out,
uint16_t *value_length_in_out,
GAPLEClient client, bool *has_more_out) {
return 0;
}
bool gatt_client_subscriptions_get_notification_header(GAPLEClient client,
GATTBufferedNotificationHeader *header_out) {
return false;
}
void gatt_client_subscriptions_reschedule(GAPLEClient c) {
}
void launcher_task_add_callback(CallbackEventCallback callback, void *data) {
// Use fake_system_task as mock:
system_task_add_callback(callback, data);
}
void ppogatt_create(void) {
}
void ppogatt_destroy(void) {
}
void ppogatt_handle_buffer_empty(void) {
}
void bt_driver_reconnect_try_now(bool ignore_paused) {
}
void gatt_client_op_cleanup(GAPLEClient client) {
}
void ppogatt_reset_disconnect_counter(void) {
}
// Fakes & Helpers
////////////////////////////////////////////////////////////////////////////////////////////////////
static const BTDeviceInternal s_test_device = {
.address = (const BTDeviceAddress) {
.octets = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
},
};
typedef enum {
TestServiceInstanceComplete = 1,
TestServiceInstanceIncomplete = 2,
TestServiceInstanceUnsupported = 3,
} TestServiceInstance;
static BLEService s_service_handles[] = {
TestServiceInstanceComplete,
TestServiceInstanceIncomplete,
TestServiceInstanceUnsupported,
};
typedef enum {
TestCharacteristicInstanceCompleteOne = 11,
TestCharacteristicInstanceCompleteTwo = 12,
TestCharacteristicInstanceIncompleteOne = 21,
TestCharacteristicInstanceUnsupported = 33,
} TestCharacteristicInstance;
Uuid gatt_client_service_get_uuid(BLEService service_ref) {
switch (service_ref) {
case TestServiceInstanceComplete:
case TestServiceInstanceIncomplete:
return s_test_service_uuid;
case TestServiceInstanceUnsupported:
default:
return UUID_INVALID;
}
}
uint8_t gatt_client_service_get_characteristics_matching_uuids(BLEService service_ref,
BLECharacteristic characteristics_out[],
const Uuid matching_characteristic_uuids[],
uint8_t num_characteristics) {
cl_assert_equal_i(num_characteristics, TestCharacteristicCount);
switch (service_ref) {
case TestServiceInstanceComplete:
characteristics_out[0] = TestCharacteristicInstanceCompleteOne;
characteristics_out[1] = TestCharacteristicInstanceCompleteTwo;
return 2;
case TestServiceInstanceIncomplete:
characteristics_out[0] = TestCharacteristicInstanceIncompleteOne;
return 1;
case TestCharacteristicInstanceUnsupported:
characteristics_out[0] = TestCharacteristicInstanceUnsupported;
return 1;
default:
return 0;
}
}
static int s_read_responses_consumed_count;
void gatt_client_consume_read_response(uintptr_t object_ref,
uint8_t value_out[],
uint16_t value_length,
GAPLEClient client) {
++s_read_responses_consumed_count;
}
static int s_services_discovered_count;
void test_client_handle_service_discovered(BLECharacteristic *characteristics) {
++s_services_discovered_count;
}
void test_client_invalidate_all_references(void) {
}
void test_client_handle_service_removed(BLECharacteristic *characteristics,
uint8_t num_characteristics) {
}
static bool s_can_handle_characteristic;
bool test_client_can_handle_characteristic(BLECharacteristic characteristic) {
return s_can_handle_characteristic;
}
void test_client_handle_write_response(BLECharacteristic characteristic, BLEGATTError error) {
}
void test_client_handle_subscribe(BLECharacteristic characteristic,
BLESubscription subscription_type, BLEGATTError error) {
}
void test_client_handle_read_or_notification(BLECharacteristic characteristic, const uint8_t *value,
size_t value_length, BLEGATTError error) {
}
// Tests
////////////////////////////////////////////////////////////////////////////////////////////////////
void test_kernel_le_client__initialize(void) {
s_services_discovered_count = 0;
s_read_responses_consumed_count = 0;
s_can_handle_characteristic = false;
kernel_le_client_init();
}
void test_kernel_le_client__cleanup(void) {
kernel_le_client_deinit();
fake_system_task_callbacks_cleanup();
}
void test_kernel_le_client__read_response_consumed_even_if_client_is_gone(void) {
// Simulate the client goes away:
s_can_handle_characteristic = false;
PebbleEvent e = (PebbleEvent) {
.type = PEBBLE_BLE_GATT_CLIENT_EVENT,
.bluetooth.le.gatt_client = {
.object_ref = TestCharacteristicInstanceCompleteOne,
.value_length = 1,
.gatt_error = BLEGATTErrorSuccess,
.subtype = PebbleBLEGATTClientEventTypeCharacteristicRead,
},
};
kernel_le_client_handle_event(&e);
cl_assert_equal_i(s_read_responses_consumed_count, 1);
// When value_length is zero, the read response should NOT be consumed:
e.bluetooth.le.gatt_client.value_length = 0;
s_read_responses_consumed_count = 0;
kernel_le_client_handle_event(&e);
cl_assert_equal_i(s_read_responses_consumed_count, 0);
}
void test_kernel_le_client__service_added(void) {
uint8_t num_services_added = ARRAY_LENGTH(s_service_handles);
PebbleBLEGATTClientServiceEventInfo *info =
kernel_malloc(sizeof(PebbleBLEGATTClientServiceEventInfo) +
(num_services_added * sizeof(BLEService)));
*info = (PebbleBLEGATTClientServiceEventInfo) {
.status = BTErrnoOK,
.type = PebbleServicesAdded,
.device = s_test_device,
};
info->services_added_data.num_services_added = num_services_added;
memcpy(info->services_added_data.services, s_service_handles, sizeof(s_service_handles));
PebbleEvent e = (PebbleEvent) {
.type = PEBBLE_BLE_GATT_CLIENT_EVENT,
.bluetooth.le.gatt_client_service = {
.info = info,
.subtype = PebbleBLEGATTClientEventTypeServiceChange,
},
};
kernel_le_client_handle_event(&e);
// Found one complete service instance:
cl_assert_equal_i(s_services_discovered_count, 1);
kernel_free(info);
}
// FIXME: PBL-27751: Improve test coverage of kernel_le_client.c
File diff suppressed because it is too large Load Diff
+182
View File
@@ -0,0 +1,182 @@
from waftools.pebble_test import clar
import waftools.ragel
def build(bld):
clar(bld,
sources_ant_glob = "src/fw/comm/ble/kernel_le_client/ams/ams_util.c "
"src/fw/comm/ble/kernel_le_client/ams/ams.c "
"src/fw/services/normal/music.c "
"tests/fakes/fake_events.c "
"tests/fakes/fake_gatt_client_operations.c "
"tests/fakes/fake_gatt_client_subscriptions.c "
"tests/fakes/fake_rtc.c",
test_sources_ant_glob = "test_ams.c")
clar(bld,
sources_ant_glob = "src/fw/comm/ble/kernel_le_client/ams/ams_util.c",
test_sources_ant_glob = "test_ams_util.c")
# TODO: the bt_driver calls here should really be a fake and not rely on
# the cc2564 module
clar(bld,
sources_ant_glob = "src/fw/comm/ble/gap_le_advert.c "
"src/fw/comm/ble/gap_le_connection.c "
"src/fw/services/common/regular_timer.c "
"src/fw/comm/internals/bt_conn_mgr.c "
"tests/fakes/fake_GAPAPI.c "
"tests/fakes/fake_rtc.c "
"tests/fakes/fake_HCIAPI.c "
"tests/fakes/fake_gap_le_connect_params.c ",
test_sources_ant_glob = "test_gap_le_advert.c",
override_includes=['dummy_board'])
clar(bld,
sources_ant_glob = "src/fw/comm/internals/bt_conn_mgr.c "
"src/fw/services/common/regular_timer.c "
"tests/fakes/fake_gap_le_connect_params.c "
"tests/fakes/fake_rtc.c ",
test_sources_ant_glob = "test_bt_conn_mgr.c")
clar(bld,
sources_ant_glob = "src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c "
"src/fw/comm/ble/gap_le_connection.c "
"src/fw/comm/ble/gatt_client_accessors.c "
"src/fw/comm/ble/gatt_client_discovery.c "
"src/fw/comm/ble/gatt_service_changed.c "
"src/fw/comm/internals/bt_conn_mgr.c "
"tests/fakes/fake_events.c "
"tests/fakes/fake_rtc.c "
"tests/fakes/fake_GATTAPI.c "
"tests/fakes/fake_GATTAPI_test_vectors.c "
"tests/fakes/fake_gap_le_connect_params.c ",
test_sources_ant_glob = "test_gatt_client_accessors.c")
clar(bld,
sources_ant_glob = "src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c "
"src/fw/comm/ble/gap_le_connection.c "
"src/fw/comm/ble/gatt_client_accessors.c "
"src/fw/comm/ble/gatt_client_discovery.c "
"src/fw/comm/ble/gatt_service_changed.c "
"src/fw/comm/internals/bt_conn_mgr.c "
"tests/fakes/fake_gap_le_connect_params.c "
"tests/fakes/fake_events.c "
"tests/fakes/fake_rtc.c "
"tests/fakes/fake_GATTAPI.c "
"tests/fakes/fake_GATTAPI_test_vectors.c ",
test_sources_ant_glob = "test_gatt_client_discovery.c")
clar(bld,
sources_ant_glob = "src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c "
"src/fw/comm/ble/gap_le_connection.c "
"src/fw/comm/ble/gap_le_task.c "
"src/fw/comm/ble/gatt_client_accessors.c "
"src/fw/comm/ble/gatt_client_discovery.c "
"src/fw/comm/ble/gatt_client_subscriptions.c "
"src/fw/comm/ble/gatt_service_changed.c "
"src/fw/comm/internals/bt_conn_mgr.c "
"tests/fakes/fake_gap_le_connect_params.c "
"tests/fakes/fake_events.c "
"tests/fakes/fake_GATTAPI.c "
"tests/fakes/fake_GATTAPI_test_vectors.c "
"tests/fakes/fake_queue.c "
"tests/fakes/fake_rtc.c ",
test_sources_ant_glob = "test_gatt_client_subscriptions.c")
clar(bld,
sources_ant_glob = "src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c "
"src/fw/comm/ble/gatt_client_discovery.c "
"src/fw/comm/ble/gatt_service_changed.c "
"src/fw/comm/internals/bt_conn_mgr.c "
"tests/fakes/fake_gap_le_connect_params.c "
"tests/fakes/fake_GATTAPI.c "
"tests/fakes/fake_GATTAPI_test_vectors.c "
"tests/fakes/fake_rtc.c",
test_sources_ant_glob = "test_gatt_service_changed_client.c")
clar(bld,
sources_ant_glob = "src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c "
"src/fw/comm/ble/gap_le_connection.c "
"src/fw/comm/ble/gatt_client_discovery.c "
"src/fw/comm/ble/gatt_service_changed.c "
"src/fw/comm/internals/bt_conn_mgr.c "
"tests/fakes/fake_gap_le_connect_params.c "
"tests/fakes/fake_GATTAPI.c "
"tests/fakes/fake_GATTAPI_test_vectors.c "
"tests/fakes/fake_rtc.c",
test_sources_ant_glob="test_gatt_service_changed_server.c")
clar(bld,
sources_ant_glob = "src/fw/comm/ble/gap_le_connect.c "
"src/fw/comm/ble/gap_le_connection.c "
"src/fw/comm/ble/gap_le_task.c "
"src/fw/comm/internals/bt_conn_mgr.c "
"tests/fakes/fake_events.c "
"tests/fakes/fake_GAPAPI.c "
"tests/fakes/fake_bluetooth_persistent_storage.c "
"tests/fakes/fake_gap_le_connect_params.c "
"tests/fakes/fake_HCIAPI.c "
"tests/fakes/fake_rtc.c ",
test_sources_ant_glob = "test_gap_le_connect.c",
override_includes=['dummy_board'])
clar(bld,
sources_ant_glob = "src/fw/util/time/mktime.c " \
"src/fw/util/buffer.c " \
"src/fw/util/time/time.c " \
"src/fw/comm/ble/kernel_le_client/ancs/ancs_util.c " \
"tests/fakes/fake_rtc.c",
test_sources_ant_glob = "test_ancs_util.c")
clar(bld,
sources_ant_glob = "src/fw/util/buffer.c " \
"src/fw/util/pstring.c " \
"src/fw/util/time/mktime.c " \
"src/fw/util/time/time.c "\
"src/fw/comm/ble/kernel_le_client/ancs/ancs_util.c " \
"src/fw/comm/ble/kernel_le_client/ancs/ancs.c " \
"src/fw/comm/ble/kernel_le_client/ancs/ancs_app_name_storage.c " \
"src/fw/services/common/regular_timer.c " \
"src/fw/util/rand/rand.c " \
"src/fw/vendor/tinymt32/tinymt32.c " \
"tests/fakes/fake_events.c " \
"tests/fakes/fake_kernel_services_notifications.c " \
"tests/fakes/fake_gatt_client_subscriptions.c " \
"tests/fakes/fake_rtc.c " \
"src/fw/services/normal/notifications/ancs/ancs_notifications.c " \
" src/fw/services/normal/notifications/ancs/ancs_notifications_util.c " \
"src/fw/services/normal/notifications/ancs/ancs_item.c " \
"src/fw/services/normal/notifications/ancs/ancs_filtering.c " \
"src/fw/services/normal/notifications/ancs/ancs_phone_call.c " \
"src/fw/services/normal/timeline/item.c " \
"src/fw/services/normal/timeline/attributes_actions.c " \
"src/fw/services/normal/timeline/attribute_group.c " \
"src/fw/services/normal/timeline/attribute.c",
test_sources_ant_glob = "test_ancs.c")
clar(bld,
sources_ant_glob="src/fw/comm/ble/kernel_le_client/kernel_le_client.c "
"src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c",
test_sources_ant_glob="test_kernel_le_client.c")
for ppogatt_version in [ 0, 1]:
clar(bld,
sources_ant_glob = "src/fw/system/hexdump.c " \
"src/fw/comm/ble/kernel_le_client/ppogatt/ppogatt.c " \
"src/fw/services/common/regular_timer.c " \
"src/fw/util/rand/rand.c "
"src/fw/vendor/tinymt32/tinymt32.c "
"tests/fakes/fake_gatt_client_operations.c " \
"tests/fakes/fake_gatt_client_subscriptions.c " \
"tests/fakes/fake_rtc.c " \
"tests/fakes/fake_session.c",
defines=["USE_PPOGATT_VERSION=%d" % ppogatt_version],
test_name='test_ppogatt_v%d' % ppogatt_version,
test_sources_ant_glob = "test_ppogatt.c")
# vim:filetype=python