Merge "Include A/AAAA records in probing packet" into main
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 9757daa..9132857 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -96,6 +96,7 @@
},
binaries: [
"clatd",
+ "netbpfload",
"ot-daemon",
],
canned_fs_config: "canned_fs_config",
diff --git a/Tethering/apex/canned_fs_config b/Tethering/apex/canned_fs_config
index 5a03347..1f5fcfa 100644
--- a/Tethering/apex/canned_fs_config
+++ b/Tethering/apex/canned_fs_config
@@ -1,2 +1,3 @@
/bin/for-system 0 1000 0750
/bin/for-system/clatd 1029 1029 06755
+/bin/netbpfload 0 0 0750
diff --git a/Tethering/lint-baseline.xml b/Tethering/lint-baseline.xml
new file mode 100644
index 0000000..37511c6
--- /dev/null
+++ b/Tethering/lint-baseline.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.netstats.provider.NetworkStatsProvider#notifyWarningReached`"
+ errorLine1=" mStatsProvider.notifyWarningReached();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/OffloadController.java"
+ line="293"
+ column="44"/>
+ </issue>
+
+</issues>
\ No newline at end of file
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
index 81d4fbe..60f2d17 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
@@ -44,6 +44,7 @@
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.net.module.util.netlink.StructNlMsgHdr;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -84,6 +85,14 @@
mOffloadHw = new OffloadHardwareInterface(mHandler, mLog, mDeps);
}
+ @After
+ public void tearDown() throws Exception {
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
+ }
+ }
+
void findConnectionOrThrow(FileDescriptor fd, InetSocketAddress local, InetSocketAddress remote)
throws Exception {
Log.d(TAG, "Looking for socket " + local + " -> " + remote);
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
index d734b74..0a2b0b8 100644
--- a/bpf_progs/block.c
+++ b/bpf_progs/block.c
@@ -24,8 +24,8 @@
#include "bpf_helpers.h"
-#define ALLOW 1
-#define DISALLOW 0
+static const int ALLOW = 1;
+static const int DISALLOW = 0;
DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t,
1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM)
@@ -57,14 +57,18 @@
return ALLOW;
}
-DEFINE_BPF_PROG_KVER("bind4/block_port", AID_ROOT, AID_SYSTEM,
- bind4_block_port, KVER(5, 4, 0))
+// the program need to be accessible/loadable by netd (from netd updatable plugin)
+#define DEFINE_NETD_RO_BPF_PROG(SECTION_NAME, the_prog, min_kver) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, min_kver, KVER_INF, \
+ BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
+ "", "netd_readonly/", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
+DEFINE_NETD_RO_BPF_PROG("bind4/block_port", bind4_block_port, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return block_port(ctx);
}
-DEFINE_BPF_PROG_KVER("bind6/block_port", AID_ROOT, AID_SYSTEM,
- bind6_block_port, KVER(5, 4, 0))
+DEFINE_NETD_RO_BPF_PROG("bind6/block_port", bind6_block_port, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return block_port(ctx);
}
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf_progs/bpf_net_helpers.h
index bb5e330..f3c7de5 100644
--- a/bpf_progs/bpf_net_helpers.h
+++ b/bpf_progs/bpf_net_helpers.h
@@ -87,36 +87,18 @@
if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
}
-// constants for passing in to 'bool shared' (for maps)
-static const bool PRIVATE = false;
-static const bool SHARED = true;
+struct egress_bool { bool egress; };
+#define INGRESS ((struct egress_bool){ .egress = false })
+#define EGRESS ((struct egress_bool){ .egress = true })
-// constants for passing in to 'bool optional' (for programs)
-static const bool MANDATORY = false;
-static const bool OPTIONAL = true;
+struct stream_bool { bool down; };
+#define UPSTREAM ((struct stream_bool){ .down = false })
+#define DOWNSTREAM ((struct stream_bool){ .down = true })
-// constants for passing in to 'bool egress'
-static const bool INGRESS = false;
-static const bool EGRESS = true;
+struct rawip_bool { bool rawip; };
+#define ETHER ((struct rawip_bool){ .rawip = false })
+#define RAWIP ((struct rawip_bool){ .rawip = true })
-// constants for passing in to 'bool downstream'
-static const bool UPSTREAM = false;
-static const bool DOWNSTREAM = true;
-
-// constants for passing in to 'bool is_ethernet'
-static const bool RAWIP = false;
-static const bool ETHER = true;
-
-// constants for passing in to 'bool updatetime'
-static const bool NO_UPDATETIME = false;
-static const bool UPDATETIME = true;
-
-// constants for passing in to ignore_on_eng / ignore_on_user / ignore_on_userdebug
-static const bool LOAD_ON_ENG = false;
-static const bool LOAD_ON_USER = false;
-static const bool LOAD_ON_USERDEBUG = false;
-static const bool IGNORE_ON_ENG = true;
-static const bool IGNORE_ON_USER = true;
-static const bool IGNORE_ON_USERDEBUG = true;
-
-#define KVER_4_14 KVER(4, 14, 0)
+struct updatetime_bool { bool updatetime; };
+#define NO_UPDATETIME ((struct updatetime_bool){ .updatetime = false })
+#define UPDATETIME ((struct updatetime_bool){ .updatetime = true })
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index 8f0ff84..addb02f 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -55,8 +55,10 @@
DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
static inline __always_inline int nat64(struct __sk_buff* skb,
- const bool is_ethernet,
- const unsigned kver) {
+ const struct rawip_bool rawip,
+ const struct kver_uint kver) {
+ const bool is_ethernet = !rawip.rawip;
+
// Require ethernet dst mac address to be our unicast address.
if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
@@ -115,7 +117,7 @@
if (proto == IPPROTO_FRAGMENT) {
// Fragment handling requires bpf_skb_adjust_room which is 4.14+
- if (kver < KVER_4_14) return TC_ACT_PIPE;
+ if (!KVER_IS_AT_LEAST(kver, 4, 14, 0)) return TC_ACT_PIPE;
// Must have (ethernet and) ipv6 header and ipv6 fragment extension header
if (data + l2_header_size + sizeof(*ip6) + sizeof(struct frag_hdr) > data_end)
@@ -233,7 +235,7 @@
//
// Note: we currently have no TreeHugger coverage for 4.9-T devices (there are no such
// Pixel or cuttlefish devices), so likely you won't notice for months if this breaks...
- if (kver >= KVER_4_14 && frag_off != htons(IP_DF)) {
+ if (KVER_IS_AT_LEAST(kver, 4, 14, 0) && frag_off != htons(IP_DF)) {
// If we're converting an IPv6 Fragment, we need to trim off 8 more bytes
// We're beyond recovery on error here... but hard to imagine how this could fail.
if (bpf_skb_adjust_room(skb, -(__s32)sizeof(struct frag_hdr), BPF_ADJ_ROOM_NET, /*flags*/0))
diff --git a/bpf_progs/dscpPolicy.c b/bpf_progs/dscpPolicy.c
index 88b50b5..e845a69 100644
--- a/bpf_progs/dscpPolicy.c
+++ b/bpf_progs/dscpPolicy.c
@@ -222,7 +222,7 @@
}
DEFINE_BPF_PROG_KVER("schedcls/set_dscp_ether", AID_ROOT, AID_SYSTEM, schedcls_set_dscp_ether,
- KVER(5, 15, 0))
+ KVER_5_15)
(struct __sk_buff* skb) {
if (skb->pkt_type != PACKET_HOST) return TC_ACT_PIPE;
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 7a48e8c..9017976 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -178,8 +178,8 @@
#define DEFINE_UPDATE_STATS(the_stats_map, TypeOfKey) \
static __always_inline inline void update_##the_stats_map(const struct __sk_buff* const skb, \
const TypeOfKey* const key, \
- const bool egress, \
- const unsigned kver) { \
+ const struct egress_bool egress, \
+ const struct kver_uint kver) { \
StatsValue* value = bpf_##the_stats_map##_lookup_elem(key); \
if (!value) { \
StatsValue newValue = {}; \
@@ -199,7 +199,7 @@
packets = (payload + mss - 1) / mss; \
bytes = tcp_overhead * packets + payload; \
} \
- if (egress) { \
+ if (egress.egress) { \
__sync_fetch_and_add(&value->txPackets, packets); \
__sync_fetch_and_add(&value->txBytes, bytes); \
} else { \
@@ -219,7 +219,7 @@
const int L3_off,
void* const to,
const int len,
- const unsigned kver) {
+ const struct kver_uint kver) {
// 'kver' (here and throughout) is the compile time guaranteed minimum kernel version,
// ie. we're building (a version of) the bpf program for kver (or newer!) kernels.
//
@@ -236,16 +236,16 @@
//
// For similar reasons this will fail with non-offloaded VLAN tags on < 4.19 kernels,
// since those extend the ethernet header from 14 to 18 bytes.
- return kver >= KVER(4, 19, 0)
+ return KVER_IS_AT_LEAST(kver, 4, 19, 0)
? bpf_skb_load_bytes_relative(skb, L3_off, to, len, BPF_HDR_START_NET)
: bpf_skb_load_bytes(skb, L3_off, to, len);
}
static __always_inline inline void do_packet_tracing(
- const struct __sk_buff* const skb, const bool egress, const uint32_t uid,
- const uint32_t tag, const bool enable_tracing, const unsigned kver) {
+ const struct __sk_buff* const skb, const struct egress_bool egress, const uint32_t uid,
+ const uint32_t tag, const bool enable_tracing, const struct kver_uint kver) {
if (!enable_tracing) return;
- if (kver < KVER(5, 8, 0)) return;
+ if (!KVER_IS_AT_LEAST(kver, 5, 8, 0)) return;
uint32_t mapKey = 0;
bool* traceConfig = bpf_packet_trace_enabled_map_lookup_elem(&mapKey);
@@ -317,8 +317,8 @@
pkt->sport = sport;
pkt->dport = dport;
- pkt->egress = egress;
- pkt->wakeup = !egress && (skb->mark & 0x80000000); // Fwmark.ingress_cpu_wakeup
+ pkt->egress = egress.egress;
+ pkt->wakeup = !egress.egress && (skb->mark & 0x80000000); // Fwmark.ingress_cpu_wakeup
pkt->ipProto = proto;
pkt->tcpFlags = flags;
pkt->ipVersion = ipVersion;
@@ -326,8 +326,9 @@
bpf_packet_trace_ringbuf_submit(pkt);
}
-static __always_inline inline bool skip_owner_match(struct __sk_buff* skb, bool egress,
- const unsigned kver) {
+static __always_inline inline bool skip_owner_match(struct __sk_buff* skb,
+ const struct egress_bool egress,
+ const struct kver_uint kver) {
uint32_t flag = 0;
if (skb->protocol == htons(ETH_P_IP)) {
uint8_t proto;
@@ -358,7 +359,7 @@
return false;
}
// Always allow RST's, and additionally allow ingress FINs
- return flag & (TCP_FLAG_RST | (egress ? 0 : TCP_FLAG_FIN)); // false on read failure
+ return flag & (TCP_FLAG_RST | (egress.egress ? 0 : TCP_FLAG_FIN)); // false on read failure
}
static __always_inline inline BpfConfig getConfig(uint32_t configKey) {
@@ -372,11 +373,11 @@
}
static __always_inline inline bool ingress_should_discard(struct __sk_buff* skb,
- const unsigned kver) {
+ const struct kver_uint kver) {
// Require 4.19, since earlier kernels don't have bpf_skb_load_bytes_relative() which
// provides relative to L3 header reads. Without that we could fetch the wrong bytes.
// Additionally earlier bpf verifiers are much harder to please.
- if (kver < KVER(4, 19, 0)) return false;
+ if (!KVER_IS_AT_LEAST(kver, 4, 19, 0)) return false;
IngressDiscardKey k = {};
if (skb->protocol == htons(ETH_P_IP)) {
@@ -401,7 +402,8 @@
}
static __always_inline inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid,
- bool egress, const unsigned kver) {
+ const struct egress_bool egress,
+ const struct kver_uint kver) {
if (is_system_uid(uid)) return PASS;
if (skip_owner_match(skb, egress, kver)) return PASS;
@@ -414,7 +416,7 @@
if (isBlockedByUidRules(enabledRules, uidRules)) return DROP;
- if (!egress && skb->ifindex != 1) {
+ if (!egress.egress && skb->ifindex != 1) {
if (ingress_should_discard(skb, kver)) return DROP;
if (uidRules & IIF_MATCH) {
if (allowed_iif && skb->ifindex != allowed_iif) {
@@ -434,8 +436,8 @@
static __always_inline inline void update_stats_with_config(const uint32_t selectedMap,
const struct __sk_buff* const skb,
const StatsKey* const key,
- const bool egress,
- const unsigned kver) {
+ const struct egress_bool egress,
+ const struct kver_uint kver) {
if (selectedMap == SELECT_MAP_A) {
update_stats_map_A(skb, key, egress, kver);
} else {
@@ -443,9 +445,10 @@
}
}
-static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb, bool egress,
+static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb,
+ const struct egress_bool egress,
const bool enable_tracing,
- const unsigned kver) {
+ const struct kver_uint kver) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
uint64_t cookie = bpf_get_socket_cookie(skb);
UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
@@ -462,7 +465,7 @@
// interface is accounted for and subject to usage restrictions.
// CLAT IPv6 TX sockets are *always* tagged with CLAT uid, see tagSocketAsClat()
// CLAT daemon receives via an untagged AF_PACKET socket.
- if (egress && uid == AID_CLAT) return PASS;
+ if (egress.egress && uid == AID_CLAT) return PASS;
int match = bpf_owner_match(skb, sock_uid, egress, kver);
@@ -478,7 +481,7 @@
}
// If an outbound packet is going to be dropped, we do not count that traffic.
- if (egress && (match == DROP)) return DROP;
+ if (egress.egress && (match == DROP)) return DROP;
StatsKey key = {.uid = uid, .tag = tag, .counterSet = 0, .ifaceIndex = skb->ifindex};
@@ -505,64 +508,64 @@
// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace_user", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_trace_user, KVER(5, 8, 0), KVER_INF,
+ bpf_cgroup_ingress_trace_user, KVER_5_8, KVER_INF,
BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
"fs_bpf_netd_readonly", "",
IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER(5, 8, 0));
+ return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
}
// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_trace, KVER(5, 8, 0), KVER_INF,
+ bpf_cgroup_ingress_trace, KVER_5_8, KVER_INF,
BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, MANDATORY,
"fs_bpf_netd_readonly", "",
LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER(5, 8, 0));
+ return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_4_19, KVER(4, 19, 0), KVER_INF)
+ bpf_cgroup_ingress_4_19, KVER_4_19, KVER_INF)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER(4, 19, 0));
+ return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_4_19);
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_14", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_4_14, KVER_NONE, KVER(4, 19, 0))
+ bpf_cgroup_ingress_4_14, KVER_NONE, KVER_4_19)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
}
// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace_user", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_trace_user, KVER(5, 8, 0), KVER_INF,
+ bpf_cgroup_egress_trace_user, KVER_5_8, KVER_INF,
BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
"fs_bpf_netd_readonly", "",
LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER(5, 8, 0));
+ return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
}
// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_trace, KVER(5, 8, 0), KVER_INF,
+ bpf_cgroup_egress_trace, KVER_5_8, KVER_INF,
BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, MANDATORY,
"fs_bpf_netd_readonly", "",
LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER(5, 8, 0));
+ return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_4_19, KVER(4, 19, 0), KVER_INF)
+ bpf_cgroup_egress_4_19, KVER_4_19, KVER_INF)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER(4, 19, 0));
+ return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_4_19);
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_14", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_4_14, KVER_NONE, KVER(4, 19, 0))
+ bpf_cgroup_egress_4_14, KVER_NONE, KVER_4_19)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_NONE);
}
@@ -637,9 +640,7 @@
return BPF_NOMATCH;
}
-DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
- KVER(4, 14, 0))
-(struct bpf_sock* sk) {
+static __always_inline inline uint8_t get_app_permissions() {
uint64_t gid_uid = bpf_get_current_uid_gid();
/*
* A given app is guaranteed to have the same app ID in all the profiles in
@@ -649,13 +650,15 @@
*/
uint32_t appId = (gid_uid & 0xffffffff) % AID_USER_OFFSET; // == PER_USER_RANGE == 100000
uint8_t* permissions = bpf_uid_permission_map_lookup_elem(&appId);
- if (!permissions) {
- // UID not in map. Default to just INTERNET permission.
- return 1;
- }
+ // if UID not in map, then default to just INTERNET permission.
+ return permissions ? *permissions : BPF_PERMISSION_INTERNET;
+}
+DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
+ KVER_4_14)
+(struct bpf_sock* sk) {
// A return value of 1 means allow, everything else means deny.
- return (*permissions & BPF_PERMISSION_INTERNET) == BPF_PERMISSION_INTERNET;
+ return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? 1 : 0;
}
LICENSE("Apache 2.0");
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index c752779..35b8eea 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -124,8 +124,12 @@
DEFINE_BPF_MAP_GRW(tether_upstream6_map, HASH, TetherUpstream6Key, Tether6Value, 64,
TETHERING_GID)
-static inline __always_inline int do_forward6(struct __sk_buff* skb, const bool is_ethernet,
- const bool downstream, const unsigned kver) {
+static inline __always_inline int do_forward6(struct __sk_buff* skb,
+ const struct rawip_bool rawip,
+ const struct stream_bool stream,
+ const struct kver_uint kver) {
+ const bool is_ethernet = !rawip.rawip;
+
// Must be meta-ethernet IPv6 frame
if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_PIPE;
@@ -184,7 +188,7 @@
TC_PUNT(NON_GLOBAL_DST);
// In the upstream direction do not forward traffic within the same /64 subnet.
- if (!downstream && (src32 == dst32) && (ip6->saddr.s6_addr32[1] == ip6->daddr.s6_addr32[1]))
+ if (!stream.down && (src32 == dst32) && (ip6->saddr.s6_addr32[1] == ip6->daddr.s6_addr32[1]))
TC_PUNT(LOCAL_SRC_DST);
TetherDownstream6Key kd = {
@@ -196,15 +200,15 @@
.iif = skb->ifindex,
.src64 = 0,
};
- if (is_ethernet) __builtin_memcpy(downstream ? kd.dstMac : ku.dstMac, eth->h_dest, ETH_ALEN);
+ if (is_ethernet) __builtin_memcpy(stream.down ? kd.dstMac : ku.dstMac, eth->h_dest, ETH_ALEN);
- Tether6Value* v = downstream ? bpf_tether_downstream6_map_lookup_elem(&kd)
- : bpf_tether_upstream6_map_lookup_elem(&ku);
+ Tether6Value* v = stream.down ? bpf_tether_downstream6_map_lookup_elem(&kd)
+ : bpf_tether_upstream6_map_lookup_elem(&ku);
// If we don't find any offload information then simply let the core stack handle it...
if (!v) return TC_ACT_PIPE;
- uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif;
+ uint32_t stat_and_limit_k = stream.down ? skb->ifindex : v->oif;
TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k);
@@ -249,7 +253,7 @@
// We do this even if TX interface is RAWIP and thus does not need an ethernet header,
// because this is easier and the kernel will strip extraneous ethernet header.
if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) {
- __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
TC_PUNT(CHANGE_HEAD_FAILED);
}
@@ -261,7 +265,7 @@
// I do not believe this can ever happen, but keep the verifier happy...
if (data + sizeof(struct ethhdr) + sizeof(*ip6) > data_end) {
- __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
TC_DROP(TOO_SHORT);
}
};
@@ -281,8 +285,8 @@
// (-ENOTSUPP) if it isn't.
bpf_csum_update(skb, 0xFFFF - ntohs(old_hl) + ntohs(new_hl));
- __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets);
- __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxPackets : &stat_v->txPackets, packets);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
// Overwrite any mac header with the new one
// For a rawip tx interface it will simply be a bunch of zeroes and later stripped.
@@ -324,26 +328,26 @@
//
// Hence, these mandatory (must load successfully) implementations for 4.14+ kernels:
DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream6_rawip_4_14, KVER(4, 14, 0))
+ sched_cls_tether_downstream6_rawip_4_14, KVER_4_14)
(struct __sk_buff* skb) {
- return do_forward6(skb, RAWIP, DOWNSTREAM, KVER(4, 14, 0));
+ return do_forward6(skb, RAWIP, DOWNSTREAM, KVER_4_14);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream6_rawip_4_14, KVER(4, 14, 0))
+ sched_cls_tether_upstream6_rawip_4_14, KVER_4_14)
(struct __sk_buff* skb) {
- return do_forward6(skb, RAWIP, UPSTREAM, KVER(4, 14, 0));
+ return do_forward6(skb, RAWIP, UPSTREAM, KVER_4_14);
}
// and define no-op stubs for pre-4.14 kernels.
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER(4, 14, 0))
+ sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER_4_14)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER(4, 14, 0))
+ sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER_4_14)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
@@ -356,9 +360,10 @@
static inline __always_inline int do_forward4_bottom(struct __sk_buff* skb,
const int l2_header_size, void* data, const void* data_end,
- struct ethhdr* eth, struct iphdr* ip, const bool is_ethernet,
- const bool downstream, const bool updatetime, const bool is_tcp,
- const unsigned kver) {
+ struct ethhdr* eth, struct iphdr* ip, const struct rawip_bool rawip,
+ const struct stream_bool stream, const struct updatetime_bool updatetime,
+ const bool is_tcp, const struct kver_uint kver) {
+ const bool is_ethernet = !rawip.rawip;
struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
@@ -416,13 +421,13 @@
};
if (is_ethernet) __builtin_memcpy(k.dstMac, eth->h_dest, ETH_ALEN);
- Tether4Value* v = downstream ? bpf_tether_downstream4_map_lookup_elem(&k)
- : bpf_tether_upstream4_map_lookup_elem(&k);
+ Tether4Value* v = stream.down ? bpf_tether_downstream4_map_lookup_elem(&k)
+ : bpf_tether_upstream4_map_lookup_elem(&k);
// If we don't find any offload information then simply let the core stack handle it...
if (!v) return TC_ACT_PIPE;
- uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif;
+ uint32_t stat_and_limit_k = stream.down ? skb->ifindex : v->oif;
TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k);
@@ -467,7 +472,7 @@
// We do this even if TX interface is RAWIP and thus does not need an ethernet header,
// because this is easier and the kernel will strip extraneous ethernet header.
if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) {
- __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
TC_PUNT(CHANGE_HEAD_FAILED);
}
@@ -481,7 +486,7 @@
// I do not believe this can ever happen, but keep the verifier happy...
if (data + sizeof(struct ethhdr) + sizeof(*ip) + (is_tcp ? sizeof(*tcph) : sizeof(*udph)) > data_end) {
- __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
TC_DROP(TOO_SHORT);
}
};
@@ -533,10 +538,10 @@
// This requires the bpf_ktime_get_boot_ns() helper which was added in 5.8,
// and backported to all Android Common Kernel 4.14+ trees.
- if (updatetime) v->last_used = bpf_ktime_get_boot_ns();
+ if (updatetime.updatetime) v->last_used = bpf_ktime_get_boot_ns();
- __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets);
- __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxPackets : &stat_v->txPackets, packets);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
// Redirect to forwarded interface.
//
@@ -547,8 +552,13 @@
return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
}
-static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
- const bool downstream, const bool updatetime, const unsigned kver) {
+static inline __always_inline int do_forward4(struct __sk_buff* skb,
+ const struct rawip_bool rawip,
+ const struct stream_bool stream,
+ const struct updatetime_bool updatetime,
+ const struct kver_uint kver) {
+ const bool is_ethernet = !rawip.rawip;
+
// Require ethernet dst mac address to be our unicast address.
if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
@@ -606,16 +616,16 @@
// in such a situation we can only support TCP. This also has the added nice benefit of
// using a separate error counter, and thus making it obvious which version of the program
// is loaded.
- if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
+ if (!updatetime.updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
// We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
// but no need to check this if !updatetime due to check immediately above.
- if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
+ if (updatetime.updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
TC_PUNT(NON_TCP_UDP);
// We want to make sure that the compiler will, in the !updatetime case, entirely optimize
// out all the non-tcp logic. Also note that at this point is_udp === !is_tcp.
- const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
+ const bool is_tcp = !updatetime.updatetime || (ip->protocol == IPPROTO_TCP);
// This is a bit of a hack to make things easier on the bpf verifier.
// (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
@@ -636,37 +646,37 @@
// if the underlying requisite kernel support (bpf_ktime_get_boot_ns) was backported.
if (is_tcp) {
return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
- is_ethernet, downstream, updatetime, /* is_tcp */ true, kver);
+ rawip, stream, updatetime, /* is_tcp */ true, kver);
} else {
return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
- is_ethernet, downstream, updatetime, /* is_tcp */ false, kver);
+ rawip, stream, updatetime, /* is_tcp */ false, kver);
}
}
// Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_rawip_5_8, KVER(5, 8, 0))
+ sched_cls_tether_downstream4_rawip_5_8, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER(5, 8, 0));
+ return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_8);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_rawip_5_8, KVER(5, 8, 0))
+ sched_cls_tether_upstream4_rawip_5_8, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER(5, 8, 0));
+ return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_8);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_ether_5_8, KVER(5, 8, 0))
+ sched_cls_tether_downstream4_ether_5_8, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER(5, 8, 0));
+ return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_8);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_ether_5_8, KVER(5, 8, 0))
+ sched_cls_tether_upstream4_ether_5_8, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER(5, 8, 0));
+ return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_8);
}
// Full featured (optional) implementations for 4.14-S, 4.19-S & 5.4-S kernels
@@ -675,33 +685,33 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_downstream4_rawip_opt,
- KVER(4, 14, 0), KVER(5, 8, 0))
+ KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_4_14);
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_upstream4_rawip_opt,
- KVER(4, 14, 0), KVER(5, 8, 0))
+ KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_4_14);
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_downstream4_ether_opt,
- KVER(4, 14, 0), KVER(5, 8, 0))
+ KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_4_14);
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_upstream4_ether_opt,
- KVER(4, 14, 0), KVER(5, 8, 0))
+ KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_4_14);
}
// Partial (TCP-only: will not update 'last_used' field) implementations for 4.14+ kernels.
@@ -719,15 +729,15 @@
// RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head().
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+ sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER(5, 4, 0));
+ return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_5_4);
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+ sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER(5, 4, 0));
+ return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_5_4);
}
// RAWIP: Optional for 4.14/4.19 (R) kernels -- which support bpf_skb_change_head().
@@ -736,31 +746,31 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_downstream4_rawip_4_14,
- KVER(4, 14, 0), KVER(5, 4, 0))
+ KVER_4_14, KVER_5_4)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_upstream4_rawip_4_14,
- KVER(4, 14, 0), KVER(5, 4, 0))
+ KVER_4_14, KVER_5_4)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_4_14);
}
// ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels.
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
+ sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
+ sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER_4_14);
}
// Placeholder (no-op) implementations for older Q kernels
@@ -768,13 +778,13 @@
// RAWIP: 4.9-P/Q, 4.14-P/Q & 4.19-Q kernels -- without bpf_skb_change_head() for tc programs
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+ sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER_5_4)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+ sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER_5_4)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
@@ -782,13 +792,13 @@
// ETHER: 4.9-P/Q kernel
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER(4, 14, 0))
+ sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER_4_14)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER(4, 14, 0))
+ sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER_4_14)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
@@ -797,17 +807,18 @@
DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, TETHERING_GID)
-static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const bool is_ethernet,
- const bool downstream) {
+static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const struct rawip_bool rawip,
+ const struct stream_bool stream) {
return XDP_PASS;
}
-static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const bool is_ethernet,
- const bool downstream) {
+static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const struct rawip_bool rawip,
+ const struct stream_bool stream) {
return XDP_PASS;
}
-static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx, const bool downstream) {
+static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx,
+ const struct stream_bool stream) {
const void* data = (void*)(long)ctx->data;
const void* data_end = (void*)(long)ctx->data_end;
const struct ethhdr* eth = data;
@@ -816,15 +827,16 @@
if ((void*)(eth + 1) > data_end) return XDP_PASS;
if (eth->h_proto == htons(ETH_P_IPV6))
- return do_xdp_forward6(ctx, ETHER, downstream);
+ return do_xdp_forward6(ctx, ETHER, stream);
if (eth->h_proto == htons(ETH_P_IP))
- return do_xdp_forward4(ctx, ETHER, downstream);
+ return do_xdp_forward4(ctx, ETHER, stream);
// Anything else we don't know how to handle...
return XDP_PASS;
}
-static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx, const bool downstream) {
+static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx,
+ const struct stream_bool stream) {
const void* data = (void*)(long)ctx->data;
const void* data_end = (void*)(long)ctx->data_end;
@@ -832,15 +844,15 @@
if (data_end - data < 1) return XDP_PASS;
const uint8_t v = (*(uint8_t*)data) >> 4;
- if (v == 6) return do_xdp_forward6(ctx, RAWIP, downstream);
- if (v == 4) return do_xdp_forward4(ctx, RAWIP, downstream);
+ if (v == 6) return do_xdp_forward6(ctx, RAWIP, stream);
+ if (v == 4) return do_xdp_forward4(ctx, RAWIP, stream);
// Anything else we don't know how to handle...
return XDP_PASS;
}
#define DEFINE_XDP_PROG(str, func) \
- DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER(5, 9, 0))(struct xdp_md *ctx)
+ DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER_5_9)(struct xdp_md *ctx)
DEFINE_XDP_PROG("xdp/tether_downstream_ether",
xdp_tether_downstream_ether) {
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index 68469c8..70b08b7 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -49,7 +49,7 @@
DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, TETHERING_GID)
DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", TETHERING_UID, TETHERING_GID,
- xdp_test, KVER(5, 9, 0))
+ xdp_test, KVER_5_9)
(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
diff --git a/common/Android.bp b/common/Android.bp
index ff4de11..c982431 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -19,6 +19,12 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
+build = ["TrunkStable.bp"]
+
+// This is a placeholder comment to avoid merge conflicts
+// as the above target may not exist
+// depending on the branch
+
java_library {
name: "connectivity-net-module-utils-bpf",
srcs: [
diff --git a/common/TrunkStable.bp b/common/TrunkStable.bp
new file mode 100644
index 0000000..56938fc
--- /dev/null
+++ b/common/TrunkStable.bp
@@ -0,0 +1,26 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+//
+
+aconfig_declarations {
+ name: "com.android.net.flags-aconfig",
+ package: "com.android.net.flags",
+ srcs: ["flags.aconfig"],
+}
+
+java_aconfig_library {
+ name: "connectivity_flags_aconfig_lib",
+ aconfig_declarations: "com.android.net.flags-aconfig",
+}
diff --git a/common/flags.aconfig b/common/flags.aconfig
new file mode 100644
index 0000000..cadc44f
--- /dev/null
+++ b/common/flags.aconfig
@@ -0,0 +1,15 @@
+package: "com.android.net.flags"
+
+flag {
+ name: "track_multiple_network_activities"
+ namespace: "android_core_networking"
+ description: "NetworkActivityTracker tracks multiple networks including non default networks"
+ bug: "267870186"
+}
+
+flag {
+ name: "forbidden_capability"
+ namespace: "android_core_networking"
+ description: "This flag controls the forbidden capability API"
+ bug: "302997505"
+}
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index ea465aa..f6b5657 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -59,12 +59,30 @@
}
public class NearbyManager {
+ method public void queryOffloadCapability(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.nearby.OffloadCapability>);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void startBroadcast(@NonNull android.nearby.BroadcastRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.BroadcastCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int startScan(@NonNull android.nearby.ScanRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.ScanCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopBroadcast(@NonNull android.nearby.BroadcastCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopScan(@NonNull android.nearby.ScanCallback);
}
+ public final class OffloadCapability implements android.os.Parcelable {
+ method public int describeContents();
+ method public long getVersion();
+ method public boolean isFastPairSupported();
+ method public boolean isNearbyShareSupported();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.nearby.OffloadCapability> CREATOR;
+ }
+
+ public static final class OffloadCapability.Builder {
+ ctor public OffloadCapability.Builder();
+ method @NonNull public android.nearby.OffloadCapability build();
+ method @NonNull public android.nearby.OffloadCapability.Builder setFastPairSupported(boolean);
+ method @NonNull public android.nearby.OffloadCapability.Builder setNearbyShareSupported(boolean);
+ method @NonNull public android.nearby.OffloadCapability.Builder setVersion(long);
+ }
+
public final class PresenceBroadcastRequest extends android.nearby.BroadcastRequest implements android.os.Parcelable {
method public int describeContents();
method @NonNull public java.util.List<java.lang.Integer> getActions();
@@ -175,8 +193,14 @@
public interface ScanCallback {
method public void onDiscovered(@NonNull android.nearby.NearbyDevice);
+ method public default void onError(int);
method public void onLost(@NonNull android.nearby.NearbyDevice);
method public void onUpdated(@NonNull android.nearby.NearbyDevice);
+ field public static final int ERROR_INVALID_ARGUMENT = 2; // 0x2
+ field public static final int ERROR_PERMISSION_DENIED = 3; // 0x3
+ field public static final int ERROR_RESOURCE_EXHAUSTED = 4; // 0x4
+ field public static final int ERROR_UNKNOWN = 0; // 0x0
+ field public static final int ERROR_UNSUPPORTED = 1; // 0x1
}
public abstract class ScanFilter {
@@ -191,6 +215,7 @@
method public int getScanType();
method @NonNull public android.os.WorkSource getWorkSource();
method public boolean isBleEnabled();
+ method public boolean isOffloadOnly();
method public static boolean isValidScanMode(int);
method public static boolean isValidScanType(int);
method @NonNull public static String scanModeToString(int);
@@ -209,6 +234,7 @@
method @NonNull public android.nearby.ScanRequest.Builder addScanFilter(@NonNull android.nearby.ScanFilter);
method @NonNull public android.nearby.ScanRequest build();
method @NonNull public android.nearby.ScanRequest.Builder setBleEnabled(boolean);
+ method @NonNull public android.nearby.ScanRequest.Builder setOffloadOnly(boolean);
method @NonNull public android.nearby.ScanRequest.Builder setScanMode(int);
method @NonNull public android.nearby.ScanRequest.Builder setScanType(int);
method @NonNull @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public android.nearby.ScanRequest.Builder setWorkSource(@Nullable android.os.WorkSource);
@@ -279,6 +305,7 @@
ctor public NetworkStats(long, int);
method @NonNull public android.net.NetworkStats add(@NonNull android.net.NetworkStats);
method @NonNull public android.net.NetworkStats addEntry(@NonNull android.net.NetworkStats.Entry);
+ method public android.net.NetworkStats clone();
method public int describeContents();
method @NonNull public java.util.Iterator<android.net.NetworkStats.Entry> iterator();
method @NonNull public android.net.NetworkStats subtract(@NonNull android.net.NetworkStats);
diff --git a/framework-t/src/android/net/EthernetNetworkSpecifier.java b/framework-t/src/android/net/EthernetNetworkSpecifier.java
index e4d6e24..90c0361 100644
--- a/framework-t/src/android/net/EthernetNetworkSpecifier.java
+++ b/framework-t/src/android/net/EthernetNetworkSpecifier.java
@@ -26,8 +26,6 @@
/**
* A {@link NetworkSpecifier} used to identify ethernet interfaces.
- *
- * @see EthernetManager
*/
public final class EthernetNetworkSpecifier extends NetworkSpecifier implements Parcelable {
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 4a2ed8a..e812024 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -94,6 +94,7 @@
}
public final class DscpPolicy implements android.os.Parcelable {
+ method public int describeContents();
method @Nullable public java.net.InetAddress getDestinationAddress();
method @Nullable public android.util.Range<java.lang.Integer> getDestinationPortRange();
method public int getDscpValue();
@@ -101,6 +102,7 @@
method public int getProtocol();
method @Nullable public java.net.InetAddress getSourceAddress();
method public int getSourcePort();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.DscpPolicy> CREATOR;
field public static final int PROTOCOL_ANY = -1; // 0xffffffff
field public static final int SOURCE_PORT_ANY = -1; // 0xffffffff
diff --git a/framework/lint-baseline.xml b/framework/lint-baseline.xml
new file mode 100644
index 0000000..f68aad7
--- /dev/null
+++ b/framework/lint-baseline.xml
@@ -0,0 +1,367 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.IpSecManager.UdpEncapsulationSocket#getResourceId`"
+ errorLine1=" return new NattSocketKeepalive(mService, network, dup, socket.getResourceId(), source,"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="2456"
+ column="71"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.Proxy#setHttpProxyConfiguration`"
+ errorLine1=" Proxy.setHttpProxyConfiguration(getInstance().getDefaultProxy());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="5323"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+ errorLine1=" if (!Build.isDebuggable()) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivitySettingsManager.java"
+ line="1072"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+ errorLine1=" final int end = nextUser.getUid(0 /* appId */) - 1;"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
+ line="50"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+ errorLine1=" final int start = user.getUid(0 /* appId */);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
+ line="49"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.provider.Settings#checkAndNoteWriteSettingsOperation`"
+ errorLine1=" return Settings.checkAndNoteWriteSettingsOperation(context, uid, callingPackage,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="2799"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#clearDnsCache`"
+ errorLine1=" InetAddress.clearDnsCache();"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="5329"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#getAllByNameOnNet`"
+ errorLine1=" return InetAddress.getAllByNameOnNet(host, getNetIdForResolv());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="145"
+ column="28"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#getByNameOnNet`"
+ errorLine1=" return InetAddress.getByNameOnNet(host, getNetIdForResolv());"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="158"
+ column="28"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(is);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="168"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" if (failed) IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="216"
+ column="45"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" if (failed) IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="241"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" if (failed) IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="254"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" if (failed) IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="272"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(bis);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="391"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(bos);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="406"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
+ line="181"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
+ line="373"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(zos);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="175"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#isNumericAddress`"
+ errorLine1=" return InetAddressUtils.isNumericAddress(address);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
+ line="46"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#parseNumericAddress`"
+ errorLine1=" return InetAddressUtils.parseNumericAddress(address);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
+ line="63"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#dispatchNetworkConfigurationChange`"
+ errorLine1=" NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="5332"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#getInstance`"
+ errorLine1=" NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="5332"
+ column="36"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#createInstance`"
+ errorLine1=" HttpURLConnectionFactory urlConnectionFactory = HttpURLConnectionFactory.createInstance();"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="302"
+ column="82"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#openConnection`"
+ errorLine1=" return urlConnectionFactory.openConnection(url, socketFactory, proxy);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="372"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#setDns`"
+ errorLine1=" urlConnectionFactory.setDns(dnsLookup); // Let traffic go via dnsLookup"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="303"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#setNewConnectionPool`"
+ errorLine1=" urlConnectionFactory.setNewConnectionPool(httpMaxConnections,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="305"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
+ errorLine1=" return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
+ line="525"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
+ errorLine1=" return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
+ line="525"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.telephony.data.EpsBearerQosSessionAttributes`"
+ errorLine1=" (EpsBearerQosSessionAttributes)attributes));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
+ line="1421"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.telephony.data.EpsBearerQosSessionAttributes`"
+ errorLine1=" if (attributes instanceof EpsBearerQosSessionAttributes) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
+ line="1418"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.telephony.data.NrQosSessionAttributes`"
+ errorLine1=" (NrQosSessionAttributes)attributes));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
+ line="1425"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.telephony.data.NrQosSessionAttributes`"
+ errorLine1=" } else if (attributes instanceof NrQosSessionAttributes) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
+ line="1422"
+ column="42"/>
+ </issue>
+
+</issues>
\ No newline at end of file
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index abda1fa..f959114 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -29,6 +29,9 @@
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.net.ConnectivityManager.NetworkCallback;
+// Can't be imported because aconfig tooling doesn't exist on udc-mainline-prod yet
+// See inner class Flags which mimics this for the time being
+// import android.net.flags.Flags;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
@@ -121,6 +124,14 @@
public final class NetworkCapabilities implements Parcelable {
private static final String TAG = "NetworkCapabilities";
+ // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+ // available here
+ /** @hide */
+ public static class Flags {
+ static final String FLAG_FORBIDDEN_CAPABILITY =
+ "com.android.net.flags.forbidden_capability";
+ }
+
/**
* Mechanism to support redaction of fields in NetworkCapabilities that are guarded by specific
* app permissions.
@@ -442,6 +453,7 @@
NET_CAPABILITY_MMTEL,
NET_CAPABILITY_PRIORITIZE_LATENCY,
NET_CAPABILITY_PRIORITIZE_BANDWIDTH,
+ NET_CAPABILITY_LOCAL_NETWORK,
})
public @interface NetCapability { }
@@ -703,7 +715,21 @@
*/
public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35;
- private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_PRIORITIZE_BANDWIDTH;
+ /**
+ * This is a local network, e.g. a tethering downstream or a P2P direct network.
+ *
+ * <p>
+ * Note that local networks are not sent to callbacks by default. To receive callbacks about
+ * them, the {@link NetworkRequest} instance must be prepared to see them, either by
+ * adding the capability with {@link NetworkRequest.Builder#addCapability}, by removing
+ * this forbidden capability with {@link NetworkRequest.Builder#removeForbiddenCapability},
+ * or by clearing all capabilites with {@link NetworkRequest.Builder#clearCapabilities()}.
+ * </p>
+ * @hide
+ */
+ public static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
+
+ private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_LOCAL_NETWORK;
// Set all bits up to the MAX_NET_CAPABILITY-th bit
private static final long ALL_VALID_CAPABILITIES = (2L << MAX_NET_CAPABILITY) - 1;
@@ -793,6 +819,10 @@
* Adds the given capability to this {@code NetworkCapability} instance.
* Note that when searching for a network to satisfy a request, all capabilities
* requested must be satisfied.
+ * <p>
+ * If the capability was previously added to the list of forbidden capabilities (either
+ * by default or added using {@link #addForbiddenCapability(int)}), then it will be removed
+ * from the list of forbidden capabilities as well.
*
* @param capability the capability to be added.
* @return This NetworkCapabilities instance, to facilitate chaining.
@@ -801,8 +831,7 @@
public @NonNull NetworkCapabilities addCapability(@NetCapability int capability) {
// If the given capability was previously added to the list of forbidden capabilities
// then the capability will also be removed from the list of forbidden capabilities.
- // TODO: Consider adding forbidden capabilities to the public API and mention this
- // in the documentation.
+ // TODO: Add forbidden capabilities to the public API
checkValidCapability(capability);
mNetworkCapabilities |= 1L << capability;
// remove from forbidden capability list
@@ -845,7 +874,7 @@
}
/**
- * Removes (if found) the given forbidden capability from this {@code NetworkCapability}
+ * Removes (if found) the given forbidden capability from this {@link NetworkCapabilities}
* instance that were added via addForbiddenCapability(int) or setCapabilities(int[], int[]).
*
* @param capability the capability to be removed.
@@ -859,6 +888,16 @@
}
/**
+ * Removes all forbidden capabilities from this {@link NetworkCapabilities} instance.
+ * @return This NetworkCapabilities instance, to facilitate chaining.
+ * @hide
+ */
+ public @NonNull NetworkCapabilities removeAllForbiddenCapabilities() {
+ mForbiddenNetworkCapabilities = 0;
+ return this;
+ }
+
+ /**
* Sets (or clears) the given capability on this {@link NetworkCapabilities}
* instance.
* @hide
@@ -901,6 +940,8 @@
* @return an array of forbidden capability values for this instance.
* @hide
*/
+ @NonNull
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
public @NetCapability int[] getForbiddenCapabilities() {
return BitUtils.unpackBits(mForbiddenNetworkCapabilities);
}
@@ -1000,7 +1041,7 @@
/**
* Tests for the presence of a capability on this instance.
*
- * @param capability the capabilities to be tested for.
+ * @param capability the capability to be tested for.
* @return {@code true} if set on this instance.
*/
public boolean hasCapability(@NetCapability int capability) {
@@ -1008,19 +1049,27 @@
&& ((mNetworkCapabilities & (1L << capability)) != 0);
}
- /** @hide */
+ /**
+ * Tests for the presence of a forbidden capability on this instance.
+ *
+ * @param capability the capability to be tested for.
+ * @return {@code true} if this capability is set forbidden on this instance.
+ * @hide
+ */
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
public boolean hasForbiddenCapability(@NetCapability int capability) {
return isValidCapability(capability)
&& ((mForbiddenNetworkCapabilities & (1L << capability)) != 0);
}
/**
- * Check if this NetworkCapabilities has system managed capabilities or not.
+ * Check if this NetworkCapabilities has connectivity-managed capabilities or not.
* @hide
*/
public boolean hasConnectivityManagedCapability() {
- return ((mNetworkCapabilities & CONNECTIVITY_MANAGED_CAPABILITIES) != 0);
+ return (mNetworkCapabilities & CONNECTIVITY_MANAGED_CAPABILITIES) != 0
+ || mForbiddenNetworkCapabilities != 0;
}
/**
@@ -2500,6 +2549,7 @@
case NET_CAPABILITY_MMTEL: return "MMTEL";
case NET_CAPABILITY_PRIORITIZE_LATENCY: return "PRIORITIZE_LATENCY";
case NET_CAPABILITY_PRIORITIZE_BANDWIDTH: return "PRIORITIZE_BANDWIDTH";
+ case NET_CAPABILITY_LOCAL_NETWORK: return "LOCAL_NETWORK";
default: return Integer.toString(capability);
}
}
@@ -2889,6 +2939,44 @@
}
/**
+ * Adds the given capability to the list of forbidden capabilities.
+ *
+ * A network with a capability will not match a {@link NetworkCapabilities} or
+ * {@link NetworkRequest} which has said capability set as forbidden. For example, if
+ * a request has NET_CAPABILITY_INTERNET in the list of forbidden capabilities, networks
+ * with NET_CAPABILITY_INTERNET will not match the request.
+ *
+ * If the capability was previously added to the list of required capabilities (for
+ * example, it was there by default or added using {@link #addCapability(int)} method), then
+ * it will be removed from the list of required capabilities as well.
+ *
+ * @param capability the capability
+ * @return this builder
+ * @hide
+ */
+ @NonNull
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
+ public Builder addForbiddenCapability(@NetCapability final int capability) {
+ mCaps.addForbiddenCapability(capability);
+ return this;
+ }
+
+ /**
+ * Removes the given capability from the list of forbidden capabilities.
+ *
+ * @see #addForbiddenCapability(int)
+ * @param capability the capability
+ * @return this builder
+ * @hide
+ */
+ @NonNull
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
+ public Builder removeForbiddenCapability(@NetCapability final int capability) {
+ mCaps.removeForbiddenCapability(capability);
+ return this;
+ }
+
+ /**
* Adds the given enterprise capability identifier.
* Note that when searching for a network to satisfy a request, all capabilities identifier
* requested must be satisfied. Enterprise capability identifier is applicable only
@@ -3235,4 +3323,4 @@
return new NetworkCapabilities(mCaps);
}
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 6c351d0..9824faa 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -20,6 +20,7 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -39,6 +40,8 @@
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
+// TODO : replace with android.net.flags.Flags when aconfig is supported on udc-mainline-prod
+// import android.net.NetworkCapabilities.Flags;
import android.net.NetworkCapabilities.NetCapability;
import android.net.NetworkCapabilities.Transport;
import android.os.Build;
@@ -281,6 +284,15 @@
NET_CAPABILITY_TRUSTED,
NET_CAPABILITY_VALIDATED);
+ /**
+ * Capabilities that are forbidden by default.
+ * Forbidden capabilities only make sense in NetworkRequest, not for network agents.
+ * Therefore these capabilities are only in NetworkRequest.
+ */
+ private static final int[] DEFAULT_FORBIDDEN_CAPABILITIES = new int[] {
+ NET_CAPABILITY_LOCAL_NETWORK
+ };
+
private final NetworkCapabilities mNetworkCapabilities;
// A boolean that represents whether the NOT_VCN_MANAGED capability should be deduced when
@@ -296,6 +308,16 @@
// it for apps that do not have the NETWORK_SETTINGS permission.
mNetworkCapabilities = new NetworkCapabilities();
mNetworkCapabilities.setSingleUid(Process.myUid());
+ // Default forbidden capabilities are foremost meant to help with backward
+ // compatibility. When adding new types of network identified by a capability that
+ // might confuse older apps, a default forbidden capability will have apps not see
+ // these networks unless they explicitly ask for it.
+ // If the app called clearCapabilities() it will see everything, but then it
+ // can be argued that it's fair to send them too, since it asked for everything
+ // explicitly.
+ for (final int forbiddenCap : DEFAULT_FORBIDDEN_CAPABILITIES) {
+ mNetworkCapabilities.addForbiddenCapability(forbiddenCap);
+ }
}
/**
@@ -408,6 +430,7 @@
@NonNull
@SuppressLint("MissingGetterMatchingBuilder")
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
public Builder addForbiddenCapability(@NetworkCapabilities.NetCapability int capability) {
mNetworkCapabilities.addForbiddenCapability(capability);
return this;
@@ -424,6 +447,7 @@
@NonNull
@SuppressLint("BuilderSetStyle")
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
public Builder removeForbiddenCapability(
@NetworkCapabilities.NetCapability int capability) {
mNetworkCapabilities.removeForbiddenCapability(capability);
@@ -433,6 +457,7 @@
/**
* Completely clears all the {@code NetworkCapabilities} from this builder instance,
* removing even the capabilities that are set by default when the object is constructed.
+ * Also removes any set forbidden capabilities.
*
* @return The builder to facilitate chaining.
*/
@@ -721,6 +746,7 @@
* @hide
*/
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public instead of @SystemApi
public boolean hasForbiddenCapability(@NetCapability int capability) {
return networkCapabilities.hasForbiddenCapability(capability);
}
@@ -843,6 +869,7 @@
*/
@NonNull
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public instead of @SystemApi
public @NetCapability int[] getForbiddenCapabilities() {
// No need to make a defensive copy here as NC#getForbiddenCapabilities() already returns
// a new array.
diff --git a/framework/src/android/net/NetworkScore.java b/framework/src/android/net/NetworkScore.java
index 815e2b0..00382f6 100644
--- a/framework/src/android/net/NetworkScore.java
+++ b/framework/src/android/net/NetworkScore.java
@@ -44,7 +44,9 @@
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
KEEP_CONNECTED_NONE,
- KEEP_CONNECTED_FOR_HANDOVER
+ KEEP_CONNECTED_FOR_HANDOVER,
+ KEEP_CONNECTED_FOR_TEST,
+ KEEP_CONNECTED_DOWNSTREAM_NETWORK
})
public @interface KeepConnectedReason { }
@@ -57,6 +59,18 @@
* is being considered for handover.
*/
public static final int KEEP_CONNECTED_FOR_HANDOVER = 1;
+ /**
+ * Keep this network connected even if there is no outstanding request for it, because it
+ * is used in a test and it's not necessarily easy to file the right request for it.
+ * @hide
+ */
+ public static final int KEEP_CONNECTED_FOR_TEST = 2;
+ /**
+ * Keep this network connected even if there is no outstanding request for it, because
+ * it is a downstream network.
+ * @hide
+ */
+ public static final int KEEP_CONNECTED_DOWNSTREAM_NETWORK = 3;
// Agent-managed policies
// This network should lose to a wifi that has ever been validated
diff --git a/nearby/README.md b/nearby/README.md
index 6925dc4..8451882 100644
--- a/nearby/README.md
+++ b/nearby/README.md
@@ -29,6 +29,20 @@
$ aidegen .
# This will launch Intellij project for Nearby module.
```
+Note, the setup above may fail to index classes defined in proto, such
+that all classes defined in proto shows red in IDE and cannot be auto-completed.
+To fix, you can mannually add jar files generated from proto to the class path
+as below. First, find the jar file of presence proto with
+```sh
+ls $ANDROID_BUILD_TOP/out/soong/.intermediates/packages/modules/Connectivity/nearby/service/proto/presence-lite-protos/android_common/combined/presence-lite-protos.jar
+```
+Then, add the jar in IDE as below.
+1. Menu: File > Project Structure
+2. Select Modules at the left panel and select the Dependencies tab.
+3. Select the + icon and select 1 JARs or Directories option.
+4. Select the JAR file found above, make sure it is checked in the beginning square.
+5. Click the OK button.
+6. Restart the IDE to re-index.
## Build and Install
@@ -40,3 +54,23 @@
--output /tmp/tethering.apex
$ adb install -r /tmp/tethering.apex
```
+
+## Build and Install from tm-mainline-prod branch
+When build and flash the APEX from tm-mainline-prod, you may see the error below.
+```
+[INSTALL_FAILED_VERSION_DOWNGRADE: Downgrade of APEX package com.google.android.tethering is not allowed. Active version: 990090000 attempted: 339990000])
+```
+This is because the device is flashed with AOSP built from master or other branches, which has
+prebuilt APEX with higher version. We can use root access to replace the prebuilt APEX with the APEX
+built from tm-mainline-prod as below.
+1. adb root && adb remount; adb shell mount -orw,remount /system/apex
+2. cp tethering.next.apex com.google.android.tethering.apex
+3. adb push com.google.android.tethering.apex /system/apex/
+4. adb reboot
+After the steps above, the APEX can be reinstalled with adb install -r.
+(More APEX background can be found in https://source.android.com/docs/core/ota/apex#using-an-apex.)
+
+## Build APEX to support multiple platforms
+If you need to flash the APEX to different devices, Pixel 6, Pixel 7, or even devices from OEM, you
+can share the APEX by ```source build/envsetup.sh && lunch aosp_arm64-userdebug```. This can avoid
+ re-compiling for different targets.
diff --git a/nearby/framework/java/android/nearby/DataElement.java b/nearby/framework/java/android/nearby/DataElement.java
index 6fa5fb5..10c1132 100644
--- a/nearby/framework/java/android/nearby/DataElement.java
+++ b/nearby/framework/java/android/nearby/DataElement.java
@@ -16,13 +16,17 @@
package android.nearby;
+import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.internal.util.Preconditions;
+import java.util.Arrays;
+import java.util.Objects;
/**
* Represents a data element in Nearby Presence.
@@ -35,11 +39,95 @@
private final int mKey;
private final byte[] mValue;
+ /** @hide */
+ @IntDef({
+ DataType.BLE_SERVICE_DATA,
+ DataType.BLE_ADDRESS,
+ DataType.SALT,
+ DataType.PRIVATE_IDENTITY,
+ DataType.TRUSTED_IDENTITY,
+ DataType.PUBLIC_IDENTITY,
+ DataType.PROVISIONED_IDENTITY,
+ DataType.TX_POWER,
+ DataType.ACTION,
+ DataType.MODEL_ID,
+ DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER,
+ DataType.ACCOUNT_KEY_DATA,
+ DataType.CONNECTION_STATUS,
+ DataType.BATTERY,
+ DataType.SCAN_MODE,
+ DataType.TEST_DE_BEGIN,
+ DataType.TEST_DE_END
+ })
+ public @interface DataType {
+ int BLE_SERVICE_DATA = 100;
+ int BLE_ADDRESS = 101;
+ // This is to indicate if the scan is offload only
+ int SCAN_MODE = 102;
+ int SALT = 0;
+ int PRIVATE_IDENTITY = 1;
+ int TRUSTED_IDENTITY = 2;
+ int PUBLIC_IDENTITY = 3;
+ int PROVISIONED_IDENTITY = 4;
+ int TX_POWER = 5;
+ int ACTION = 6;
+ int MODEL_ID = 7;
+ int EDDYSTONE_EPHEMERAL_IDENTIFIER = 8;
+ int ACCOUNT_KEY_DATA = 9;
+ int CONNECTION_STATUS = 10;
+ int BATTERY = 11;
+ // Reserves test DE ranges from {@link DataElement.DataType#TEST_DE_BEGIN}
+ // to {@link DataElement.DataType#TEST_DE_END}, inclusive.
+ // Reserves 128 Test DEs.
+ int TEST_DE_BEGIN = Integer.MAX_VALUE - 127; // 2147483520
+ int TEST_DE_END = Integer.MAX_VALUE; // 2147483647
+ }
+
+ /**
+ * @hide
+ */
+ public static boolean isValidType(int type) {
+ return type == DataType.BLE_SERVICE_DATA
+ || type == DataType.ACCOUNT_KEY_DATA
+ || type == DataType.BLE_ADDRESS
+ || type == DataType.SCAN_MODE
+ || type == DataType.SALT
+ || type == DataType.PRIVATE_IDENTITY
+ || type == DataType.TRUSTED_IDENTITY
+ || type == DataType.PUBLIC_IDENTITY
+ || type == DataType.PROVISIONED_IDENTITY
+ || type == DataType.TX_POWER
+ || type == DataType.ACTION
+ || type == DataType.MODEL_ID
+ || type == DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER
+ || type == DataType.CONNECTION_STATUS
+ || type == DataType.BATTERY;
+ }
+
+ /**
+ * @return {@code true} if this is identity type.
+ * @hide
+ */
+ public boolean isIdentityDataType() {
+ return mKey == DataType.PRIVATE_IDENTITY
+ || mKey == DataType.TRUSTED_IDENTITY
+ || mKey == DataType.PUBLIC_IDENTITY
+ || mKey == DataType.PROVISIONED_IDENTITY;
+ }
+
+ /**
+ * @return {@code true} if this is test data element type.
+ * @hide
+ */
+ public static boolean isTestDeType(int type) {
+ return type >= DataType.TEST_DE_BEGIN && type <= DataType.TEST_DE_END;
+ }
+
/**
* Constructs a {@link DataElement}.
*/
public DataElement(int key, @NonNull byte[] value) {
- Preconditions.checkState(value != null, "value cannot be null");
+ Preconditions.checkArgument(value != null, "value cannot be null");
mKey = key;
mValue = value;
}
@@ -61,6 +149,20 @@
};
@Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj instanceof DataElement) {
+ return mKey == ((DataElement) obj).mKey
+ && Arrays.equals(mValue, ((DataElement) obj).mValue);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mKey, Arrays.hashCode(mValue));
+ }
+
+ @Override
public int describeContents() {
return 0;
}
diff --git a/nearby/framework/java/android/nearby/INearbyManager.aidl b/nearby/framework/java/android/nearby/INearbyManager.aidl
index 0291fff..7af271e 100644
--- a/nearby/framework/java/android/nearby/INearbyManager.aidl
+++ b/nearby/framework/java/android/nearby/INearbyManager.aidl
@@ -20,6 +20,7 @@
import android.nearby.IScanListener;
import android.nearby.BroadcastRequestParcelable;
import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
/**
* Interface for communicating with the nearby services.
@@ -37,4 +38,6 @@
in IBroadcastListener callback, String packageName, @nullable String attributionTag);
void stopBroadcast(in IBroadcastListener callback, String packageName, @nullable String attributionTag);
+
+ void queryOffloadCapability(in IOffloadCallback callback) ;
}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/IScanListener.aidl b/nearby/framework/java/android/nearby/IScanListener.aidl
index 3e3b107..80563b7 100644
--- a/nearby/framework/java/android/nearby/IScanListener.aidl
+++ b/nearby/framework/java/android/nearby/IScanListener.aidl
@@ -34,5 +34,5 @@
void onLost(in NearbyDeviceParcelable nearbyDeviceParcelable);
/** Reports when there is an error during scanning. */
- void onError();
+ void onError(in int errorCode);
}
diff --git a/nearby/framework/java/android/nearby/NearbyDevice.java b/nearby/framework/java/android/nearby/NearbyDevice.java
index 538940c..e8fcc28 100644
--- a/nearby/framework/java/android/nearby/NearbyDevice.java
+++ b/nearby/framework/java/android/nearby/NearbyDevice.java
@@ -21,11 +21,13 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.util.ArraySet;
import com.android.internal.util.Preconditions;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
/**
* A class represents a device that can be discovered by multiple mediums.
@@ -123,13 +125,17 @@
@Override
public boolean equals(Object other) {
- if (other instanceof NearbyDevice) {
- NearbyDevice otherDevice = (NearbyDevice) other;
- return Objects.equals(mName, otherDevice.mName)
- && mMediums == otherDevice.mMediums
- && mRssi == otherDevice.mRssi;
+ if (!(other instanceof NearbyDevice)) {
+ return false;
}
- return false;
+ NearbyDevice otherDevice = (NearbyDevice) other;
+ Set<Integer> mediumSet = new ArraySet<>(mMediums);
+ Set<Integer> otherMediumSet = new ArraySet<>(otherDevice.mMediums);
+ if (!mediumSet.equals(otherMediumSet)) {
+ return false;
+ }
+
+ return Objects.equals(mName, otherDevice.mName) && mRssi == otherDevice.mRssi;
}
@Override
diff --git a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
index 8f44091..8fb9650 100644
--- a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
+++ b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
@@ -46,6 +46,7 @@
@Override
public NearbyDeviceParcelable createFromParcel(Parcel in) {
Builder builder = new Builder();
+ builder.setDeviceId(in.readLong());
builder.setScanType(in.readInt());
if (in.readInt() == 1) {
builder.setName(in.readString());
@@ -76,6 +77,17 @@
in.readByteArray(salt);
builder.setData(salt);
}
+ if (in.readInt() == 1) {
+ builder.setPresenceDevice(in.readParcelable(
+ PresenceDevice.class.getClassLoader(),
+ PresenceDevice.class));
+ }
+ if (in.readInt() == 1) {
+ int encryptionKeyTagLength = in.readInt();
+ byte[] keyTag = new byte[encryptionKeyTagLength];
+ in.readByteArray(keyTag);
+ builder.setData(keyTag);
+ }
return builder.build();
}
@@ -85,6 +97,7 @@
}
};
+ private final long mDeviceId;
@ScanRequest.ScanType int mScanType;
@Nullable private final String mName;
@NearbyDevice.Medium private final int mMedium;
@@ -96,8 +109,11 @@
@Nullable private final String mFastPairModelId;
@Nullable private final byte[] mData;
@Nullable private final byte[] mSalt;
+ @Nullable private final PresenceDevice mPresenceDevice;
+ @Nullable private final byte[] mEncryptionKeyTag;
private NearbyDeviceParcelable(
+ long deviceId,
@ScanRequest.ScanType int scanType,
@Nullable String name,
int medium,
@@ -108,7 +124,10 @@
@Nullable String fastPairModelId,
@Nullable String bluetoothAddress,
@Nullable byte[] data,
- @Nullable byte[] salt) {
+ @Nullable byte[] salt,
+ @Nullable PresenceDevice presenceDevice,
+ @Nullable byte[] encryptionKeyTag) {
+ mDeviceId = deviceId;
mScanType = scanType;
mName = name;
mMedium = medium;
@@ -120,6 +139,8 @@
mBluetoothAddress = bluetoothAddress;
mData = data;
mSalt = salt;
+ mPresenceDevice = presenceDevice;
+ mEncryptionKeyTag = encryptionKeyTag;
}
/** No special parcel contents. */
@@ -136,6 +157,7 @@
*/
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeLong(mDeviceId);
dest.writeInt(mScanType);
dest.writeInt(mName == null ? 0 : 1);
if (mName != null) {
@@ -164,13 +186,24 @@
dest.writeInt(mSalt.length);
dest.writeByteArray(mSalt);
}
+ dest.writeInt(mPresenceDevice == null ? 0 : 1);
+ if (mPresenceDevice != null) {
+ dest.writeParcelable(mPresenceDevice, /* parcelableFlags= */ 0);
+ }
+ dest.writeInt(mEncryptionKeyTag == null ? 0 : 1);
+ if (mEncryptionKeyTag != null) {
+ dest.writeInt(mEncryptionKeyTag.length);
+ dest.writeByteArray(mEncryptionKeyTag);
+ }
}
/** Returns a string representation of this ScanRequest. */
@Override
public String toString() {
return "NearbyDeviceParcelable["
- + "scanType="
+ + "deviceId="
+ + mDeviceId
+ + ", scanType="
+ mScanType
+ ", name="
+ mName
@@ -197,20 +230,25 @@
public boolean equals(Object other) {
if (other instanceof NearbyDeviceParcelable) {
NearbyDeviceParcelable otherNearbyDeviceParcelable = (NearbyDeviceParcelable) other;
- return mScanType == otherNearbyDeviceParcelable.mScanType
+ return mDeviceId == otherNearbyDeviceParcelable.mDeviceId
+ && mScanType == otherNearbyDeviceParcelable.mScanType
&& (Objects.equals(mName, otherNearbyDeviceParcelable.mName))
&& (mMedium == otherNearbyDeviceParcelable.mMedium)
&& (mTxPower == otherNearbyDeviceParcelable.mTxPower)
&& (mRssi == otherNearbyDeviceParcelable.mRssi)
&& (mAction == otherNearbyDeviceParcelable.mAction)
&& (Objects.equals(
- mPublicCredential, otherNearbyDeviceParcelable.mPublicCredential))
+ mPublicCredential, otherNearbyDeviceParcelable.mPublicCredential))
&& (Objects.equals(
- mBluetoothAddress, otherNearbyDeviceParcelable.mBluetoothAddress))
+ mBluetoothAddress, otherNearbyDeviceParcelable.mBluetoothAddress))
&& (Objects.equals(
- mFastPairModelId, otherNearbyDeviceParcelable.mFastPairModelId))
+ mFastPairModelId, otherNearbyDeviceParcelable.mFastPairModelId))
&& (Arrays.equals(mData, otherNearbyDeviceParcelable.mData))
- && (Arrays.equals(mSalt, otherNearbyDeviceParcelable.mSalt));
+ && (Arrays.equals(mSalt, otherNearbyDeviceParcelable.mSalt))
+ && (Objects.equals(
+ mPresenceDevice, otherNearbyDeviceParcelable.mPresenceDevice))
+ && (Arrays.equals(
+ mEncryptionKeyTag, otherNearbyDeviceParcelable.mEncryptionKeyTag));
}
return false;
}
@@ -218,6 +256,7 @@
@Override
public int hashCode() {
return Objects.hash(
+ mDeviceId,
mScanType,
mName,
mMedium,
@@ -227,7 +266,19 @@
mBluetoothAddress,
mFastPairModelId,
Arrays.hashCode(mData),
- Arrays.hashCode(mSalt));
+ Arrays.hashCode(mSalt),
+ mPresenceDevice,
+ Arrays.hashCode(mEncryptionKeyTag));
+ }
+
+ /**
+ * The id of the device.
+ * <p>This id is not a hardware id. It may rotate based on the remote device's broadcasts.
+ *
+ * @hide
+ */
+ public long getDeviceId() {
+ return mDeviceId;
}
/**
@@ -351,8 +402,29 @@
return mSalt;
}
+ /**
+ * Gets the {@link PresenceDevice} Nearby Presence device. This field is
+ * for Fast Pair client only.
+ */
+ @Nullable
+ public PresenceDevice getPresenceDevice() {
+ return mPresenceDevice;
+ }
+
+ /**
+ * Gets the encryption key tag calculated from advertisement
+ * Returns {@code null} if the data is not encrypted or this is not a Presence device.
+ *
+ * Used in Presence.
+ */
+ @Nullable
+ public byte[] getEncryptionKeyTag() {
+ return mEncryptionKeyTag;
+ }
+
/** Builder class for {@link NearbyDeviceParcelable}. */
public static final class Builder {
+ private long mDeviceId = -1;
@Nullable private String mName;
@NearbyDevice.Medium private int mMedium;
private int mTxPower;
@@ -364,6 +436,14 @@
@Nullable private String mBluetoothAddress;
@Nullable private byte[] mData;
@Nullable private byte[] mSalt;
+ @Nullable private PresenceDevice mPresenceDevice;
+ @Nullable private byte[] mEncryptionKeyTag;
+
+ /** Sets the id of the device. */
+ public Builder setDeviceId(long deviceId) {
+ this.mDeviceId = deviceId;
+ return this;
+ }
/**
* Sets the scan type of the NearbyDeviceParcelable.
@@ -469,7 +549,7 @@
/**
* Sets the scanned raw data.
*
- * @param data Data the scan. For example, {@link ScanRecord#getServiceData()} if scanned by
+ * @param data raw data scanned, like {@link ScanRecord#getServiceData()} if scanned by
* Bluetooth.
*/
@NonNull
@@ -479,6 +559,17 @@
}
/**
+ * Sets the encryption key tag calculated from the advertisement.
+ *
+ * @param encryptionKeyTag calculated from identity scanned from the advertisement
+ */
+ @NonNull
+ public Builder setEncryptionKeyTag(@Nullable byte[] encryptionKeyTag) {
+ mEncryptionKeyTag = encryptionKeyTag;
+ return this;
+ }
+
+ /**
* Sets the slat in the advertisement from the Nearby Presence device.
*
* @param salt in the advertisement from the Nearby Presence device.
@@ -489,10 +580,22 @@
return this;
}
+ /**
+ * Sets the {@link PresenceDevice} if there is any.
+ * The current {@link NearbyDeviceParcelable} can be seen as the wrapper of the
+ * {@link PresenceDevice}.
+ */
+ @Nullable
+ public Builder setPresenceDevice(@Nullable PresenceDevice presenceDevice) {
+ mPresenceDevice = presenceDevice;
+ return this;
+ }
+
/** Builds a ScanResult. */
@NonNull
public NearbyDeviceParcelable build() {
return new NearbyDeviceParcelable(
+ mDeviceId,
mScanType,
mName,
mMedium,
@@ -503,7 +606,9 @@
mFastPairModelId,
mBluetoothAddress,
mData,
- mSalt);
+ mSalt,
+ mPresenceDevice,
+ mEncryptionKeyTag);
}
}
}
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 106c290..a70b303 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -26,6 +26,7 @@
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.content.Context;
+import android.nearby.aidl.IOffloadCallback;
import android.os.RemoteException;
import android.provider.Settings;
import android.util.Log;
@@ -37,6 +38,7 @@
import java.util.Objects;
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* This class provides a way to perform Nearby related operations such as scanning, broadcasting
@@ -62,7 +64,7 @@
ScanStatus.ERROR,
})
public @interface ScanStatus {
- // Default, invalid state.
+ // The undetermined status, some modules may be initializing. Retry is suggested.
int UNKNOWN = 0;
// The successful state.
int SUCCESS = 1;
@@ -73,6 +75,7 @@
private static final String TAG = "NearbyManager";
/**
+ * TODO(b/286137024): Remove this when CTS R5 is rolled out.
* Whether allows Fast Pair to scan.
*
* (0 = disabled, 1 = enabled)
@@ -103,6 +106,9 @@
mService = service;
}
+ // This can be null when NearbyDeviceParcelable field not set for Presence device
+ // or the scan type is not recognized.
+ @Nullable
private static NearbyDevice toClientNearbyDevice(
NearbyDeviceParcelable nearbyDeviceParcelable,
@ScanRequest.ScanType int scanType) {
@@ -118,23 +124,12 @@
}
if (scanType == ScanRequest.SCAN_TYPE_NEARBY_PRESENCE) {
- PublicCredential publicCredential = nearbyDeviceParcelable.getPublicCredential();
- if (publicCredential == null) {
- return null;
+ PresenceDevice presenceDevice = nearbyDeviceParcelable.getPresenceDevice();
+ if (presenceDevice == null) {
+ Log.e(TAG,
+ "Cannot find any Presence device in discovered NearbyDeviceParcelable");
}
- byte[] salt = nearbyDeviceParcelable.getSalt();
- if (salt == null) {
- salt = new byte[0];
- }
- return new PresenceDevice.Builder(
- // Use the public credential hash as the device Id.
- String.valueOf(publicCredential.hashCode()),
- salt,
- publicCredential.getSecretId(),
- publicCredential.getEncryptedMetadata())
- .setRssi(nearbyDeviceParcelable.getRssi())
- .addMedium(nearbyDeviceParcelable.getMedium())
- .build();
+ return presenceDevice;
}
return null;
}
@@ -278,29 +273,42 @@
}
/**
- * Read from {@link Settings} whether Fast Pair scan is enabled.
+ * Query offload capability in a device. The query is asynchronous and result is called back
+ * in {@link Consumer}, which is set to true if offload is supported.
*
- * @param context the {@link Context} to query the setting
- * @return whether the Fast Pair is enabled
- * @hide
+ * @param executor the callback will take place on this {@link Executor}
+ * @param callback the callback invoked with {@link OffloadCapability}
*/
- public static boolean getFastPairScanEnabled(@NonNull Context context) {
- final int enabled = Settings.Secure.getInt(
- context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, 0);
- return enabled != 0;
+ public void queryOffloadCapability(@NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<OffloadCapability> callback) {
+ try {
+ mService.queryOffloadCapability(new OffloadTransport(executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
- /**
- * Write into {@link Settings} whether Fast Pair scan is enabled
- *
- * @param context the {@link Context} to set the setting
- * @param enable whether the Fast Pair scan should be enabled
- * @hide
- */
- @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
- public static void setFastPairScanEnabled(@NonNull Context context, boolean enable) {
- Settings.Secure.putInt(
- context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, enable ? 1 : 0);
+ private static class OffloadTransport extends IOffloadCallback.Stub {
+
+ private final Executor mExecutor;
+ // Null when cancelled
+ volatile @Nullable Consumer<OffloadCapability> mConsumer;
+
+ OffloadTransport(Executor executor, Consumer<OffloadCapability> consumer) {
+ Preconditions.checkArgument(executor != null, "illegal null executor");
+ Preconditions.checkArgument(consumer != null, "illegal null consumer");
+ mExecutor = executor;
+ mConsumer = consumer;
+ }
+
+ @Override
+ public void onQueryComplete(OffloadCapability capability) {
+ mExecutor.execute(() -> {
+ if (mConsumer != null) {
+ mConsumer.accept(capability);
+ }
+ });
+ }
}
private static class ScanListenerTransport extends IScanListener.Stub {
@@ -339,9 +347,9 @@
public void onDiscovered(NearbyDeviceParcelable nearbyDeviceParcelable)
throws RemoteException {
mExecutor.execute(() -> {
- if (mScanCallback != null) {
- mScanCallback.onDiscovered(
- toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
+ NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
+ if (mScanCallback != null && nearbyDevice != null) {
+ mScanCallback.onDiscovered(nearbyDevice);
}
});
}
@@ -350,7 +358,8 @@
public void onUpdated(NearbyDeviceParcelable nearbyDeviceParcelable)
throws RemoteException {
mExecutor.execute(() -> {
- if (mScanCallback != null) {
+ NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
+ if (mScanCallback != null && nearbyDevice != null) {
mScanCallback.onUpdated(
toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
}
@@ -360,7 +369,8 @@
@Override
public void onLost(NearbyDeviceParcelable nearbyDeviceParcelable) throws RemoteException {
mExecutor.execute(() -> {
- if (mScanCallback != null) {
+ NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
+ if (mScanCallback != null && nearbyDevice != null) {
mScanCallback.onLost(
toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
}
@@ -368,10 +378,10 @@
}
@Override
- public void onError() {
+ public void onError(int errorCode) {
mExecutor.execute(() -> {
if (mScanCallback != null) {
- Log.e("NearbyManager", "onError: There is an error in scan.");
+ mScanCallback.onError(errorCode);
}
});
}
@@ -410,4 +420,35 @@
});
}
}
+
+ /**
+ * TODO(b/286137024): Remove this when CTS R5 is rolled out.
+ * Read from {@link Settings} whether Fast Pair scan is enabled.
+ *
+ * @param context the {@link Context} to query the setting
+ * @return whether the Fast Pair is enabled
+ * @hide
+ */
+ public static boolean getFastPairScanEnabled(@NonNull Context context) {
+ final int enabled = Settings.Secure.getInt(
+ context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, 0);
+ return enabled != 0;
+ }
+
+ /**
+ * TODO(b/286137024): Remove this when CTS R5 is rolled out.
+ * Write into {@link Settings} whether Fast Pair scan is enabled
+ *
+ * @param context the {@link Context} to set the setting
+ * @param enable whether the Fast Pair scan should be enabled
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ public static void setFastPairScanEnabled(@NonNull Context context, boolean enable) {
+ Settings.Secure.putInt(
+ context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, enable ? 1 : 0);
+ Log.v(TAG, String.format(
+ "successfully %s Fast Pair scan", enable ? "enables" : "disables"));
+ }
+
}
diff --git a/nearby/framework/java/android/nearby/OffloadCapability.aidl b/nearby/framework/java/android/nearby/OffloadCapability.aidl
new file mode 100644
index 0000000..fe1c45e
--- /dev/null
+++ b/nearby/framework/java/android/nearby/OffloadCapability.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023, The Android Open Source Project
+ *
+ * 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.
+ */
+
+package android.nearby;
+
+/**
+ * A class that can describe what offload functions are available.
+ *
+ * {@hide}
+ */
+parcelable OffloadCapability;
+
diff --git a/nearby/framework/java/android/nearby/OffloadCapability.java b/nearby/framework/java/android/nearby/OffloadCapability.java
new file mode 100644
index 0000000..9071c1c
--- /dev/null
+++ b/nearby/framework/java/android/nearby/OffloadCapability.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package android.nearby;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * A class that can describe what offload functions are available.
+ *
+ * @hide
+ */
+@SystemApi
+public final class OffloadCapability implements Parcelable {
+ private final boolean mFastPairSupported;
+ private final boolean mNearbyShareSupported;
+ private final long mVersion;
+
+ public boolean isFastPairSupported() {
+ return mFastPairSupported;
+ }
+
+ public boolean isNearbyShareSupported() {
+ return mNearbyShareSupported;
+ }
+
+ public long getVersion() {
+ return mVersion;
+ }
+
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mFastPairSupported);
+ dest.writeBoolean(mNearbyShareSupported);
+ dest.writeLong(mVersion);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<OffloadCapability> CREATOR = new Creator<OffloadCapability>() {
+ @Override
+ public OffloadCapability createFromParcel(Parcel in) {
+ boolean isFastPairSupported = in.readBoolean();
+ boolean isNearbyShareSupported = in.readBoolean();
+ long version = in.readLong();
+ return new Builder()
+ .setFastPairSupported(isFastPairSupported)
+ .setNearbyShareSupported(isNearbyShareSupported)
+ .setVersion(version)
+ .build();
+ }
+
+ @Override
+ public OffloadCapability[] newArray(int size) {
+ return new OffloadCapability[size];
+ }
+ };
+
+ private OffloadCapability(boolean fastPairSupported, boolean nearbyShareSupported,
+ long version) {
+ mFastPairSupported = fastPairSupported;
+ mNearbyShareSupported = nearbyShareSupported;
+ mVersion = version;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof OffloadCapability)) return false;
+ OffloadCapability that = (OffloadCapability) o;
+ return isFastPairSupported() == that.isFastPairSupported()
+ && isNearbyShareSupported() == that.isNearbyShareSupported()
+ && getVersion() == that.getVersion();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(isFastPairSupported(), isNearbyShareSupported(), getVersion());
+ }
+
+ @Override
+ public String toString() {
+ return "OffloadCapability{"
+ + "fastPairSupported=" + mFastPairSupported
+ + ", nearbyShareSupported=" + mNearbyShareSupported
+ + ", version=" + mVersion
+ + '}';
+ }
+
+ /**
+ * Builder class for {@link OffloadCapability}.
+ */
+ public static final class Builder {
+ private boolean mFastPairSupported;
+ private boolean mNearbyShareSupported;
+ private long mVersion;
+
+ /**
+ * Sets if the Nearby Share feature is supported
+ *
+ * @param fastPairSupported {@code true} if the Fast Pair feature is supported
+ */
+ @NonNull
+ public Builder setFastPairSupported(boolean fastPairSupported) {
+ mFastPairSupported = fastPairSupported;
+ return this;
+ }
+
+ /**
+ * Sets if the Nearby Share feature is supported.
+ *
+ * @param nearbyShareSupported {@code true} if the Nearby Share feature is supported
+ */
+ @NonNull
+ public Builder setNearbyShareSupported(boolean nearbyShareSupported) {
+ mNearbyShareSupported = nearbyShareSupported;
+ return this;
+ }
+
+ /**
+ * Sets the version number of Nearby Offload.
+ *
+ * @param version Nearby Offload version number
+ */
+ @NonNull
+ public Builder setVersion(long version) {
+ mVersion = version;
+ return this;
+ }
+
+ /**
+ * Builds an OffloadCapability object.
+ */
+ @NonNull
+ public OffloadCapability build() {
+ return new OffloadCapability(mFastPairSupported, mNearbyShareSupported, mVersion);
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/PresenceDevice.java b/nearby/framework/java/android/nearby/PresenceDevice.java
index cb406e4..b5d9ad4 100644
--- a/nearby/framework/java/android/nearby/PresenceDevice.java
+++ b/nearby/framework/java/android/nearby/PresenceDevice.java
@@ -26,6 +26,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -134,6 +135,54 @@
return mExtendedProperties;
}
+ /**
+ * This can only be hidden because this is the System API,
+ * which cannot be changed in T timeline.
+ *
+ * @hide
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof PresenceDevice) {
+ PresenceDevice otherDevice = (PresenceDevice) other;
+ if (super.equals(otherDevice)) {
+ return Arrays.equals(mSalt, otherDevice.mSalt)
+ && Arrays.equals(mSecretId, otherDevice.mSecretId)
+ && Arrays.equals(mEncryptedIdentity, otherDevice.mEncryptedIdentity)
+ && Objects.equals(mDeviceId, otherDevice.mDeviceId)
+ && mDeviceType == otherDevice.mDeviceType
+ && Objects.equals(mDeviceImageUrl, otherDevice.mDeviceImageUrl)
+ && mDiscoveryTimestampMillis == otherDevice.mDiscoveryTimestampMillis
+ && Objects.equals(mExtendedProperties, otherDevice.mExtendedProperties);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This can only be hidden because this is the System API,
+ * which cannot be changed in T timeline.
+ *
+ * @hide
+ *
+ * @return The unique hash value of the {@link PresenceDevice}
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ getName(),
+ getMediums(),
+ getRssi(),
+ Arrays.hashCode(mSalt),
+ Arrays.hashCode(mSecretId),
+ Arrays.hashCode(mEncryptedIdentity),
+ mDeviceId,
+ mDeviceType,
+ mDeviceImageUrl,
+ mDiscoveryTimestampMillis,
+ mExtendedProperties);
+ }
+
private PresenceDevice(String deviceName, List<Integer> mMediums, int rssi, String deviceId,
byte[] salt, byte[] secretId, byte[] encryptedIdentity, int deviceType,
String deviceImageUrl, long discoveryTimestampMillis,
@@ -326,7 +375,6 @@
return this;
}
-
/**
* Sets the image url of the discovered Presence device.
*
@@ -338,7 +386,6 @@
return this;
}
-
/**
* Sets discovery timestamp, the clock is based on elapsed time.
*
@@ -350,7 +397,6 @@
return this;
}
-
/**
* Adds an extended property of the discovered presence device.
*
diff --git a/nearby/framework/java/android/nearby/PresenceScanFilter.java b/nearby/framework/java/android/nearby/PresenceScanFilter.java
index f0c3c06..50e97b4 100644
--- a/nearby/framework/java/android/nearby/PresenceScanFilter.java
+++ b/nearby/framework/java/android/nearby/PresenceScanFilter.java
@@ -71,7 +71,7 @@
super(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE, rssiThreshold);
mCredentials = new ArrayList<>(credentials);
mPresenceActions = new ArrayList<>(presenceActions);
- mExtendedProperties = extendedProperties;
+ mExtendedProperties = new ArrayList<>(extendedProperties);
}
private PresenceScanFilter(Parcel in) {
@@ -132,7 +132,7 @@
}
dest.writeInt(mExtendedProperties.size());
if (!mExtendedProperties.isEmpty()) {
- dest.writeList(mExtendedProperties);
+ dest.writeParcelableList(mExtendedProperties, 0);
}
}
diff --git a/nearby/framework/java/android/nearby/ScanCallback.java b/nearby/framework/java/android/nearby/ScanCallback.java
index 1b1b4bc..7b66607 100644
--- a/nearby/framework/java/android/nearby/ScanCallback.java
+++ b/nearby/framework/java/android/nearby/ScanCallback.java
@@ -16,9 +16,13 @@
package android.nearby;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.SystemApi;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Reports newly discovered devices.
* Note: The frequency of the callback is dependent on whether the caller
@@ -31,6 +35,37 @@
*/
@SystemApi
public interface ScanCallback {
+
+ /** General error code for scan. */
+ int ERROR_UNKNOWN = 0;
+
+ /**
+ * Scan failed as the request is not supported.
+ */
+ int ERROR_UNSUPPORTED = 1;
+
+ /**
+ * Invalid argument such as out-of-range, illegal format etc.
+ */
+ int ERROR_INVALID_ARGUMENT = 2;
+
+ /**
+ * Request from clients who do not have permissions.
+ */
+ int ERROR_PERMISSION_DENIED = 3;
+
+ /**
+ * Request cannot be fulfilled due to limited resource.
+ */
+ int ERROR_RESOURCE_EXHAUSTED = 4;
+
+ /** @hide **/
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ERROR_UNKNOWN, ERROR_UNSUPPORTED, ERROR_INVALID_ARGUMENT, ERROR_PERMISSION_DENIED,
+ ERROR_RESOURCE_EXHAUSTED})
+ @interface ErrorCode {
+ }
+
/**
* Reports a {@link NearbyDevice} being discovered.
*
@@ -51,4 +86,11 @@
* @param device {@link NearbyDevice} that is lost.
*/
void onLost(@NonNull NearbyDevice device);
+
+ /**
+ * Notifies clients of error from the scan.
+ *
+ * @param errorCode defined by Nearby
+ */
+ default void onError(@ErrorCode int errorCode) {}
}
diff --git a/nearby/framework/java/android/nearby/ScanRequest.java b/nearby/framework/java/android/nearby/ScanRequest.java
index c717ac7..61cbf39 100644
--- a/nearby/framework/java/android/nearby/ScanRequest.java
+++ b/nearby/framework/java/android/nearby/ScanRequest.java
@@ -22,6 +22,7 @@
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
+import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.WorkSource;
@@ -33,6 +34,8 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* An encapsulation of various parameters for requesting nearby scans.
@@ -62,6 +65,12 @@
*/
public static final int SCAN_MODE_NO_POWER = -1;
/**
+ * A special scan mode to indicate that client only wants to use CHRE to scan.
+ *
+ * @hide
+ */
+ public static final int SCAN_MODE_CHRE_ONLY = 3;
+ /**
* Used to read a ScanRequest from a Parcel.
*/
@NonNull
@@ -72,6 +81,7 @@
.setScanType(in.readInt())
.setScanMode(in.readInt())
.setBleEnabled(in.readBoolean())
+ .setOffloadOnly(in.readBoolean())
.setWorkSource(in.readTypedObject(WorkSource.CREATOR));
final int size = in.readInt();
for (int i = 0; i < size; i++) {
@@ -89,14 +99,16 @@
private final @ScanType int mScanType;
private final @ScanMode int mScanMode;
private final boolean mBleEnabled;
+ private final boolean mOffloadOnly;
private final @NonNull WorkSource mWorkSource;
private final List<ScanFilter> mScanFilters;
private ScanRequest(@ScanType int scanType, @ScanMode int scanMode, boolean bleEnabled,
- @NonNull WorkSource workSource, List<ScanFilter> scanFilters) {
+ boolean offloadOnly, @NonNull WorkSource workSource, List<ScanFilter> scanFilters) {
mScanType = scanType;
mScanMode = scanMode;
mBleEnabled = bleEnabled;
+ mOffloadOnly = offloadOnly;
mWorkSource = workSource;
mScanFilters = scanFilters;
}
@@ -162,6 +174,13 @@
}
/**
+ * Returns if CHRE enabled for scanning.
+ */
+ public boolean isOffloadOnly() {
+ return mOffloadOnly;
+ }
+
+ /**
* Returns Scan Filters for this request.
*/
@NonNull
@@ -197,7 +216,13 @@
stringBuilder.append("Request[")
.append("scanType=").append(mScanType);
stringBuilder.append(", scanMode=").append(scanModeToString(mScanMode));
- stringBuilder.append(", enableBle=").append(mBleEnabled);
+ // TODO(b/286137024): Remove this when CTS R5 is rolled out.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ stringBuilder.append(", bleEnabled=").append(mBleEnabled);
+ stringBuilder.append(", offloadOnly=").append(mOffloadOnly);
+ } else {
+ stringBuilder.append(", enableBle=").append(mBleEnabled);
+ }
stringBuilder.append(", workSource=").append(mWorkSource);
stringBuilder.append(", scanFilters=").append(mScanFilters);
stringBuilder.append("]");
@@ -209,6 +234,7 @@
dest.writeInt(mScanType);
dest.writeInt(mScanMode);
dest.writeBoolean(mBleEnabled);
+ dest.writeBoolean(mOffloadOnly);
dest.writeTypedObject(mWorkSource, /* parcelableFlags= */0);
final int size = mScanFilters.size();
dest.writeInt(size);
@@ -224,6 +250,7 @@
return mScanType == otherRequest.mScanType
&& (mScanMode == otherRequest.mScanMode)
&& (mBleEnabled == otherRequest.mBleEnabled)
+ && (mOffloadOnly == otherRequest.mOffloadOnly)
&& (Objects.equals(mWorkSource, otherRequest.mWorkSource));
}
return false;
@@ -231,7 +258,7 @@
@Override
public int hashCode() {
- return Objects.hash(mScanType, mScanMode, mBleEnabled, mWorkSource);
+ return Objects.hash(mScanType, mScanMode, mBleEnabled, mOffloadOnly, mWorkSource);
}
/** @hide **/
@@ -254,6 +281,7 @@
private @ScanMode int mScanMode;
private boolean mBleEnabled;
+ private boolean mOffloadOnly;
private WorkSource mWorkSource;
private List<ScanFilter> mScanFilters;
@@ -261,6 +289,7 @@
public Builder() {
mScanType = INVALID_SCAN_TYPE;
mBleEnabled = true;
+ mOffloadOnly = false;
mWorkSource = new WorkSource();
mScanFilters = new ArrayList<>();
}
@@ -301,6 +330,22 @@
}
/**
+ * By default, a scan request can be served by either offload or
+ * non-offload implementation, depending on the resource available in the device.
+ *
+ * A client can explicitly request a scan to be served by offload only.
+ * Before the request, the client should query the offload capability by
+ * using {@link NearbyManager#queryOffloadCapability(Executor, Consumer)}}. Otherwise,
+ * {@link ScanCallback#ERROR_UNSUPPORTED} will be returned on devices without
+ * offload capability.
+ */
+ @NonNull
+ public Builder setOffloadOnly(boolean offloadOnly) {
+ mOffloadOnly = offloadOnly;
+ return this;
+ }
+
+ /**
* Sets the work source to use for power attribution for this scan request. Defaults to
* empty work source, which implies the caller that sends the scan request will be used
* for power attribution.
@@ -355,7 +400,8 @@
Preconditions.checkState(isValidScanMode(mScanMode),
"invalid scan mode : " + mScanMode
+ ", scan mode must be one of ScanMode#SCAN_MODE_");
- return new ScanRequest(mScanType, mScanMode, mBleEnabled, mWorkSource, mScanFilters);
+ return new ScanRequest(
+ mScanType, mScanMode, mBleEnabled, mOffloadOnly, mWorkSource, mScanFilters);
}
}
}
diff --git a/nearby/framework/java/android/nearby/aidl/IOffloadCallback.aidl b/nearby/framework/java/android/nearby/aidl/IOffloadCallback.aidl
new file mode 100644
index 0000000..8bef817
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/IOffloadCallback.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023, The Android Open Source Project
+ *
+ * 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.
+ */
+
+package android.nearby.aidl;
+
+import android.nearby.OffloadCapability;
+
+/**
+ * Listener for offload queries.
+ *
+ * {@hide}
+ */
+oneway interface IOffloadCallback {
+ /** Invokes when ContextHub transaction completes. */
+ void onQueryComplete(in OffloadCapability capability);
+}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
index 8fdac87..9b32d69 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
@@ -16,9 +16,16 @@
package com.android.server.nearby;
+import android.os.Build;
import android.provider.DeviceConfig;
-import androidx.annotation.VisibleForTesting;
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.server.nearby.managers.DiscoveryProviderManager;
+
+import java.util.concurrent.Executors;
/**
* A utility class for encapsulating Nearby feature flag configurations.
@@ -26,33 +33,123 @@
public class NearbyConfiguration {
/**
- * Flag use to enable presence legacy broadcast.
+ * Flag used to enable presence legacy broadcast.
*/
public static final String NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY =
"nearby_enable_presence_broadcast_legacy";
+ /**
+ * Flag used to for minimum nano app version to make Nearby CHRE scan work.
+ */
+ public static final String NEARBY_MAINLINE_NANO_APP_MIN_VERSION =
+ "nearby_mainline_nano_app_min_version";
+ /**
+ * Flag used to allow test mode and customization.
+ */
+ public static final String NEARBY_SUPPORT_TEST_APP = "nearby_support_test_app";
+
+ /**
+ * Flag to control which version of DiscoveryProviderManager should be used.
+ */
+ public static final String NEARBY_REFACTOR_DISCOVERY_MANAGER =
+ "nearby_refactor_discovery_manager";
+
+ private static final boolean IS_USER_BUILD = "user".equals(Build.TYPE);
+
+ private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener();
+ private final Object mDeviceConfigLock = new Object();
+
+ @GuardedBy("mDeviceConfigLock")
private boolean mEnablePresenceBroadcastLegacy;
+ @GuardedBy("mDeviceConfigLock")
+ private int mNanoAppMinVersion;
+ @GuardedBy("mDeviceConfigLock")
+ private boolean mSupportTestApp;
+ @GuardedBy("mDeviceConfigLock")
+ private boolean mRefactorDiscoveryManager;
public NearbyConfiguration() {
- mEnablePresenceBroadcastLegacy = getDeviceConfigBoolean(
- NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, false /* defaultValue */);
+ mDeviceConfigListener.start();
+ }
+ /**
+ * Returns the DeviceConfig namespace for Nearby. The {@link DeviceConfig#NAMESPACE_NEARBY} was
+ * added in UpsideDownCake, in Tiramisu, we use {@link DeviceConfig#NAMESPACE_TETHERING}.
+ */
+ public static String getNamespace() {
+ if (SdkLevel.isAtLeastU()) {
+ return DeviceConfig.NAMESPACE_NEARBY;
+ }
+ return DeviceConfig.NAMESPACE_TETHERING;
+ }
+
+ private static boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
+ final String value = getDeviceConfigProperty(name);
+ return value != null ? Boolean.parseBoolean(value) : defaultValue;
+ }
+
+ private static int getDeviceConfigInt(final String name, final int defaultValue) {
+ final String value = getDeviceConfigProperty(name);
+ return value != null ? Integer.parseInt(value) : defaultValue;
+ }
+
+ private static String getDeviceConfigProperty(String name) {
+ return DeviceConfig.getProperty(getNamespace(), name);
}
/**
* Returns whether broadcasting legacy presence spec is enabled.
*/
public boolean isPresenceBroadcastLegacyEnabled() {
- return mEnablePresenceBroadcastLegacy;
+ synchronized (mDeviceConfigLock) {
+ return mEnablePresenceBroadcastLegacy;
+ }
}
- private boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
- final String value = getDeviceConfigProperty(name);
- return value != null ? Boolean.parseBoolean(value) : defaultValue;
+ public int getNanoAppMinVersion() {
+ synchronized (mDeviceConfigLock) {
+ return mNanoAppMinVersion;
+ }
}
- @VisibleForTesting
- protected String getDeviceConfigProperty(String name) {
- return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_TETHERING, name);
+ /**
+ * @return {@code true} when in test mode and allows customization.
+ */
+ public boolean isTestAppSupported() {
+ synchronized (mDeviceConfigLock) {
+ return mSupportTestApp;
+ }
+ }
+
+ /**
+ * @return {@code true} if use {@link DiscoveryProviderManager} or use
+ * DiscoveryProviderManagerLegacy if {@code false}.
+ */
+ public boolean refactorDiscoveryManager() {
+ synchronized (mDeviceConfigLock) {
+ return mRefactorDiscoveryManager;
+ }
+ }
+
+ private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
+ public void start() {
+ DeviceConfig.addOnPropertiesChangedListener(getNamespace(),
+ Executors.newSingleThreadExecutor(), this);
+ onPropertiesChanged(DeviceConfig.getProperties(getNamespace()));
+ }
+
+ @Override
+ public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
+ synchronized (mDeviceConfigLock) {
+ mEnablePresenceBroadcastLegacy = getDeviceConfigBoolean(
+ NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, false /* defaultValue */);
+ mNanoAppMinVersion = getDeviceConfigInt(
+ NEARBY_MAINLINE_NANO_APP_MIN_VERSION, 0 /* defaultValue */);
+ mSupportTestApp = !IS_USER_BUILD && getDeviceConfigBoolean(
+ NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
+ mRefactorDiscoveryManager = getDeviceConfigBoolean(
+ NEARBY_REFACTOR_DISCOVERY_MANAGER, false /* defaultValue */);
+ }
+ }
}
}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 1220104..3c183ec 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -28,6 +28,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
import android.hardware.location.ContextHubManager;
import android.nearby.BroadcastRequestParcelable;
import android.nearby.IBroadcastListener;
@@ -35,13 +36,16 @@
import android.nearby.IScanListener;
import android.nearby.NearbyManager;
import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
import com.android.server.nearby.injector.Injector;
-import com.android.server.nearby.provider.BroadcastProviderManager;
-import com.android.server.nearby.provider.DiscoveryProviderManager;
+import com.android.server.nearby.managers.BroadcastProviderManager;
+import com.android.server.nearby.managers.DiscoveryManager;
+import com.android.server.nearby.managers.DiscoveryProviderManager;
+import com.android.server.nearby.managers.DiscoveryProviderManagerLegacy;
+import com.android.server.nearby.presence.PresenceManager;
import com.android.server.nearby.util.identity.CallerIdentity;
import com.android.server.nearby.util.permissions.BroadcastPermissions;
import com.android.server.nearby.util.permissions.DiscoveryPermissions;
@@ -49,8 +53,12 @@
/** Service implementing nearby functionality. */
public class NearbyService extends INearbyManager.Stub {
public static final String TAG = "NearbyService";
+ // Sets to true to start BLE scan from PresenceManager for manual testing.
+ public static final Boolean MANUAL_TEST = false;
private final Context mContext;
+ private final PresenceManager mPresenceManager;
+ private final NearbyConfiguration mNearbyConfiguration;
private Injector mInjector;
private final BroadcastReceiver mBluetoothReceiver =
new BroadcastReceiver() {
@@ -69,14 +77,19 @@
}
}
};
- private DiscoveryProviderManager mProviderManager;
- private BroadcastProviderManager mBroadcastProviderManager;
+ private final DiscoveryManager mDiscoveryProviderManager;
+ private final BroadcastProviderManager mBroadcastProviderManager;
public NearbyService(Context context) {
mContext = context;
mInjector = new SystemInjector(context);
- mProviderManager = new DiscoveryProviderManager(context, mInjector);
mBroadcastProviderManager = new BroadcastProviderManager(context, mInjector);
+ mPresenceManager = new PresenceManager(context);
+ mNearbyConfiguration = new NearbyConfiguration();
+ mDiscoveryProviderManager =
+ mNearbyConfiguration.refactorDiscoveryManager()
+ ? new DiscoveryProviderManager(context, mInjector)
+ : new DiscoveryProviderManagerLegacy(context, mInjector);
}
@VisibleForTesting
@@ -93,10 +106,7 @@
CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);
- if (mProviderManager.registerScanListener(scanRequest, listener, identity)) {
- return NearbyManager.ScanStatus.SUCCESS;
- }
- return NearbyManager.ScanStatus.ERROR;
+ return mDiscoveryProviderManager.registerScanListener(scanRequest, listener, identity);
}
@Override
@@ -107,7 +117,7 @@
CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);
- mProviderManager.unregisterScanListener(listener);
+ mDiscoveryProviderManager.unregisterScanListener(listener);
}
@Override
@@ -133,6 +143,11 @@
mBroadcastProviderManager.stopBroadcast(listener);
}
+ @Override
+ public void queryOffloadCapability(IOffloadCallback callback) {
+ mDiscoveryProviderManager.queryOffloadCapability(callback);
+ }
+
/**
* Called by the service initializer.
*
@@ -146,15 +161,21 @@
}
break;
case PHASE_BOOT_COMPLETED:
+ // mInjector needs to be initialized before mProviderManager.
if (mInjector instanceof SystemInjector) {
// The nearby service must be functioning after this boot phase.
((SystemInjector) mInjector).initializeBluetoothAdapter();
// Initialize ContextManager for CHRE scan.
- ((SystemInjector) mInjector).initializeContextHubManagerAdapter();
+ ((SystemInjector) mInjector).initializeContextHubManager();
}
+ mDiscoveryProviderManager.init();
mContext.registerReceiver(
mBluetoothReceiver,
new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
+ // Only enable for manual Presence test on device.
+ if (MANUAL_TEST) {
+ mPresenceManager.initiate();
+ }
break;
}
}
@@ -165,16 +186,18 @@
* throw a {@link SecurityException}.
*/
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
- private static void enforceBluetoothPrivilegedPermission(Context context) {
- context.enforceCallingOrSelfPermission(
- android.Manifest.permission.BLUETOOTH_PRIVILEGED,
- "Need BLUETOOTH PRIVILEGED permission");
+ private void enforceBluetoothPrivilegedPermission(Context context) {
+ if (!mNearbyConfiguration.isTestAppSupported()) {
+ context.enforceCallingOrSelfPermission(
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH PRIVILEGED permission");
+ }
}
private static final class SystemInjector implements Injector {
private final Context mContext;
@Nullable private BluetoothAdapter mBluetoothAdapter;
- @Nullable private ContextHubManagerAdapter mContextHubManagerAdapter;
+ @Nullable private ContextHubManager mContextHubManager;
@Nullable private AppOpsManager mAppOpsManager;
SystemInjector(Context context) {
@@ -189,8 +212,8 @@
@Override
@Nullable
- public ContextHubManagerAdapter getContextHubManagerAdapter() {
- return mContextHubManagerAdapter;
+ public ContextHubManager getContextHubManager() {
+ return mContextHubManager;
}
@Override
@@ -210,15 +233,13 @@
mBluetoothAdapter = manager.getAdapter();
}
- synchronized void initializeContextHubManagerAdapter() {
- if (mContextHubManagerAdapter != null) {
+ synchronized void initializeContextHubManager() {
+ if (mContextHubManager != null) {
return;
}
- ContextHubManager manager = mContext.getSystemService(ContextHubManager.class);
- if (manager == null) {
- return;
+ if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONTEXT_HUB)) {
+ mContextHubManager = mContext.getSystemService(ContextHubManager.class);
}
- mContextHubManagerAdapter = new ContextHubManagerAdapter(manager);
}
synchronized void initializeAppOpsManager() {
diff --git a/nearby/service/java/com/android/server/nearby/common/CancelableAlarm.java b/nearby/service/java/com/android/server/nearby/common/CancelableAlarm.java
new file mode 100644
index 0000000..00d1570
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/CancelableAlarm.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.common;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.util.Log;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+
+/**
+ * A cancelable alarm with a name. This is a simple wrapper around the logic for posting a runnable
+ * on a scheduled executor service and (possibly) later canceling it.
+ */
+public class CancelableAlarm {
+
+ private static final String TAG = "NearbyCancelableAlarm";
+
+ private final String mName;
+ private final Runnable mRunnable;
+ private final long mDelayMillis;
+ private final ScheduledExecutorService mExecutor;
+ private final boolean mIsRecurring;
+
+ // The future containing the alarm.
+ private volatile ScheduledFuture<?> mFuture;
+
+ private CancellationFlag mCancellationFlag;
+
+ private CancelableAlarm(
+ String name,
+ Runnable runnable,
+ long delayMillis,
+ ScheduledExecutorService executor,
+ boolean isRecurring) {
+ this.mName = name;
+ this.mRunnable = runnable;
+ this.mDelayMillis = delayMillis;
+ this.mExecutor = executor;
+ this.mIsRecurring = isRecurring;
+ }
+
+ /**
+ * Creates an alarm.
+ *
+ * @param name the task name
+ * @param runnable command the task to execute
+ * @param delayMillis delay the time from now to delay execution
+ * @param executor the executor that schedules commands to run
+ */
+ public static CancelableAlarm createSingleAlarm(
+ String name,
+ Runnable runnable,
+ long delayMillis,
+ ScheduledExecutorService executor) {
+ CancelableAlarm cancelableAlarm =
+ new CancelableAlarm(name, runnable, delayMillis, executor, /* isRecurring= */
+ false);
+ cancelableAlarm.scheduleExecutor();
+ return cancelableAlarm;
+ }
+
+ /**
+ * Creates a recurring alarm.
+ *
+ * @param name the task name
+ * @param runnable command the task to execute
+ * @param delayMillis delay the time from now to delay execution
+ * @param executor the executor that schedules commands to run
+ */
+ public static CancelableAlarm createRecurringAlarm(
+ String name,
+ Runnable runnable,
+ long delayMillis,
+ ScheduledExecutorService executor) {
+ CancelableAlarm cancelableAlarm =
+ new CancelableAlarm(name, runnable, delayMillis, executor, /* isRecurring= */ true);
+ cancelableAlarm.scheduleExecutor();
+ return cancelableAlarm;
+ }
+
+ // A reference to "this" should generally not be passed to another class within the constructor
+ // as it may not have completed being constructed.
+ private void scheduleExecutor() {
+ this.mFuture = mExecutor.schedule(this::processAlarm, mDelayMillis, MILLISECONDS);
+ // For tests to pass (NearbySharingChimeraServiceTest) the Cancellation Flag must come
+ // after the
+ // executor. Doing so prevents the test code from running the callback immediately.
+ this.mCancellationFlag = new CancellationFlag();
+ }
+
+ /**
+ * Cancels the pending alarm.
+ *
+ * @return true if the alarm was canceled, or false if there was a problem canceling the alarm.
+ */
+ public boolean cancel() {
+ mCancellationFlag.cancel();
+ try {
+ return mFuture.cancel(/* mayInterruptIfRunning= */ true);
+ } finally {
+ Log.v(TAG, "Canceled " + mName + " alarm");
+ }
+ }
+
+ private void processAlarm() {
+ if (mCancellationFlag.isCancelled()) {
+ Log.v(TAG, "Ignoring " + mName + " alarm because it has previously been canceled");
+ return;
+ }
+
+ Log.v(TAG, "Running " + mName + " alarm");
+ mRunnable.run();
+ if (mIsRecurring) {
+ this.mFuture = mExecutor.schedule(this::processAlarm, mDelayMillis, MILLISECONDS);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/CancellationFlag.java b/nearby/service/java/com/android/server/nearby/common/CancellationFlag.java
new file mode 100644
index 0000000..f0bb075
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/CancellationFlag.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.common;
+
+import android.util.ArraySet;
+
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A cancellation flag to mark an operation has been cancelled and should be cleaned up as soon as
+ * possible.
+ */
+public class CancellationFlag {
+
+ private final Set<OnCancelListener> mListeners = new ArraySet<>();
+ private final AtomicBoolean mIsCancelled = new AtomicBoolean();
+
+ public CancellationFlag() {
+ this(false);
+ }
+
+ public CancellationFlag(boolean isCancelled) {
+ this.mIsCancelled.set(isCancelled);
+ }
+
+ /** Set the flag as cancelled. */
+ public void cancel() {
+ if (mIsCancelled.getAndSet(true)) {
+ // Someone already cancelled. Return immediately.
+ return;
+ }
+
+ // Don't invoke OnCancelListener#onCancel inside the synchronization block, as it makes
+ // deadlocks more likely.
+ Set<OnCancelListener> clonedListeners;
+ synchronized (this) {
+ clonedListeners = new ArraySet<>(mListeners);
+ }
+ for (OnCancelListener listener : clonedListeners) {
+ listener.onCancel();
+ }
+ }
+
+ /** Returns {@code true} if the flag has been set to cancelled. */
+ public synchronized boolean isCancelled() {
+ return mIsCancelled.get();
+ }
+
+ /** Returns the flag as an {@link AtomicBoolean} object. */
+ public synchronized AtomicBoolean asAtomicBoolean() {
+ return mIsCancelled;
+ }
+
+ /** Registers a {@link OnCancelListener} to listen to cancel() event. */
+ public synchronized void registerOnCancelListener(OnCancelListener listener) {
+ mListeners.add(listener);
+ }
+
+ /**
+ * Unregisters a {@link OnCancelListener} that was previously registed through {@link
+ * #registerOnCancelListener(OnCancelListener)}.
+ */
+ public synchronized void unregisterOnCancelListener(OnCancelListener listener) {
+ mListeners.remove(listener);
+ }
+
+ /** Listens to {@link CancellationFlag#cancel()}. */
+ public interface OnCancelListener {
+ /**
+ * When CancellationFlag is canceled.
+ */
+ void onCancel();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/injector/Injector.java b/nearby/service/java/com/android/server/nearby/injector/Injector.java
index 57784a9..3152ee6 100644
--- a/nearby/service/java/com/android/server/nearby/injector/Injector.java
+++ b/nearby/service/java/com/android/server/nearby/injector/Injector.java
@@ -18,6 +18,7 @@
import android.app.AppOpsManager;
import android.bluetooth.BluetoothAdapter;
+import android.hardware.location.ContextHubManager;
/**
* Nearby dependency injector. To be used for accessing various Nearby class instances and as a
@@ -29,7 +30,7 @@
BluetoothAdapter getBluetoothAdapter();
/** Get the ContextHubManagerAdapter for ChreDiscoveryProvider to scan. */
- ContextHubManagerAdapter getContextHubManagerAdapter();
+ ContextHubManager getContextHubManager();
/** Get the AppOpsManager to control access. */
AppOpsManager getAppOpsManager();
diff --git a/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
similarity index 70%
rename from nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
rename to nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
index 3fffda5..024bff8 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package com.android.server.nearby.provider;
+package com.android.server.nearby.managers;
+import android.annotation.Nullable;
import android.content.Context;
import android.nearby.BroadcastCallback;
import android.nearby.BroadcastRequest;
@@ -27,7 +28,10 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.NearbyConfiguration;
import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.Advertisement;
+import com.android.server.nearby.presence.ExtendedAdvertisement;
import com.android.server.nearby.presence.FastAdvertisement;
+import com.android.server.nearby.provider.BleBroadcastProvider;
import com.android.server.nearby.util.ForegroundThread;
import java.util.concurrent.Executor;
@@ -66,10 +70,12 @@
public void startBroadcast(BroadcastRequest broadcastRequest, IBroadcastListener listener) {
synchronized (mLock) {
mExecutor.execute(() -> {
- NearbyConfiguration configuration = new NearbyConfiguration();
- if (!configuration.isPresenceBroadcastLegacyEnabled()) {
- reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
- return;
+ if (!mNearbyConfiguration.isTestAppSupported()) {
+ NearbyConfiguration configuration = new NearbyConfiguration();
+ if (!configuration.isPresenceBroadcastLegacyEnabled()) {
+ reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+ return;
+ }
}
if (broadcastRequest.getType() != BroadcastRequest.BROADCAST_TYPE_NEARBY_PRESENCE) {
reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
@@ -77,25 +83,38 @@
}
PresenceBroadcastRequest presenceBroadcastRequest =
(PresenceBroadcastRequest) broadcastRequest;
- if (presenceBroadcastRequest.getVersion() != BroadcastRequest.PRESENCE_VERSION_V0) {
+ Advertisement advertisement = getAdvertisement(presenceBroadcastRequest);
+ if (advertisement == null) {
+ Log.e(TAG, "Failed to start broadcast because broadcastRequest is illegal.");
reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
return;
}
- FastAdvertisement fastAdvertisement = FastAdvertisement.createFromRequest(
- presenceBroadcastRequest);
- byte[] advertisementPackets = fastAdvertisement.toBytes();
mBroadcastListener = listener;
- mBleBroadcastProvider.start(advertisementPackets, this);
+ mBleBroadcastProvider.start(presenceBroadcastRequest.getVersion(),
+ advertisement.toBytes(), this);
});
}
}
+ @Nullable
+ private Advertisement getAdvertisement(PresenceBroadcastRequest request) {
+ switch (request.getVersion()) {
+ case BroadcastRequest.PRESENCE_VERSION_V0:
+ return FastAdvertisement.createFromRequest(request);
+ case BroadcastRequest.PRESENCE_VERSION_V1:
+ return ExtendedAdvertisement.createFromRequest(request);
+ default:
+ return null;
+ }
+ }
+
/**
* Stops the nearby broadcast.
*/
public void stopBroadcast(IBroadcastListener listener) {
synchronized (mLock) {
- if (!mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
+ if (!mNearbyConfiguration.isTestAppSupported()
+ && !mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
return;
}
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryManager.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryManager.java
new file mode 100644
index 0000000..c9b9a43
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryManager.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.managers;
+
+import android.nearby.IScanListener;
+import android.nearby.NearbyManager;
+import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
+
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+/**
+ * Interface added for flagging DiscoveryProviderManager refactor. After the
+ * nearby_refactor_discovery_manager flag is fully rolled out, this can be deleted.
+ */
+public interface DiscoveryManager {
+
+ /**
+ * Registers the listener in the manager and starts scan according to the requested scan mode.
+ */
+ @NearbyManager.ScanStatus
+ int registerScanListener(ScanRequest scanRequest, IScanListener listener,
+ CallerIdentity callerIdentity);
+
+ /**
+ * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
+ */
+ void unregisterScanListener(IScanListener listener);
+
+ /** Query offload capability in a device. */
+ void queryOffloadCapability(IOffloadCallback callback);
+
+ /** Called after boot completed. */
+ void init();
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
new file mode 100644
index 0000000..0c41426
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.NearbyManager;
+import android.nearby.PresenceScanFilter;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.managers.registration.DiscoveryRegistration;
+import com.android.server.nearby.provider.AbstractDiscoveryProvider;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/** Manages all aspects of discovery providers. */
+public class DiscoveryProviderManager extends
+ ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> implements
+ AbstractDiscoveryProvider.Listener,
+ DiscoveryManager {
+
+ protected final Object mLock = new Object();
+ @VisibleForTesting
+ @Nullable
+ final ChreDiscoveryProvider mChreDiscoveryProvider;
+ private final Context mContext;
+ private final BleDiscoveryProvider mBleDiscoveryProvider;
+ private final Injector mInjector;
+ private final Executor mExecutor;
+
+ public DiscoveryProviderManager(Context context, Injector injector) {
+ Log.v(TAG, "DiscoveryProviderManager: ");
+ mContext = context;
+ mBleDiscoveryProvider = new BleDiscoveryProvider(mContext, injector);
+ mExecutor = Executors.newSingleThreadExecutor();
+ mChreDiscoveryProvider = new ChreDiscoveryProvider(mContext,
+ new ChreCommunication(injector, mContext, mExecutor), mExecutor);
+ mInjector = injector;
+ }
+
+ @VisibleForTesting
+ DiscoveryProviderManager(Context context, Executor executor, Injector injector,
+ BleDiscoveryProvider bleDiscoveryProvider,
+ ChreDiscoveryProvider chreDiscoveryProvider) {
+ mContext = context;
+ mExecutor = executor;
+ mInjector = injector;
+ mBleDiscoveryProvider = bleDiscoveryProvider;
+ mChreDiscoveryProvider = chreDiscoveryProvider;
+ }
+
+ private static boolean isChreOnly(Set<ScanFilter> scanFilters) {
+ for (ScanFilter scanFilter : scanFilters) {
+ List<DataElement> dataElements =
+ ((PresenceScanFilter) scanFilter).getExtendedProperties();
+ for (DataElement dataElement : dataElements) {
+ if (dataElement.getKey() != DataElement.DataType.SCAN_MODE) {
+ continue;
+ }
+ byte[] scanModeValue = dataElement.getValue();
+ if (scanModeValue == null || scanModeValue.length == 0) {
+ break;
+ }
+ if (Byte.toUnsignedInt(scanModeValue[0]) == ScanRequest.SCAN_MODE_CHRE_ONLY) {
+ return true;
+ }
+ }
+
+ }
+ return false;
+ }
+
+ @Override
+ public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
+ synchronized (mMultiplexerLock) {
+ Log.d(TAG, "Found device" + nearbyDevice);
+ deliverToListeners(registration -> {
+ try {
+ return registration.onNearbyDeviceDiscovered(nearbyDevice);
+ } catch (Exception e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report callback.", e);
+ return null;
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onError(int errorCode) {
+ synchronized (mMultiplexerLock) {
+ Log.e(TAG, "Error found during scanning.");
+ deliverToListeners(registration -> {
+ try {
+ return registration.reportError(errorCode);
+ } catch (Exception e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+ return null;
+ }
+ });
+ }
+ }
+
+ /** Called after boot completed. */
+ public void init() {
+ if (mInjector.getContextHubManager() != null) {
+ mChreDiscoveryProvider.init();
+ }
+ mChreDiscoveryProvider.getController().setListener(this);
+ }
+
+ /**
+ * Registers the listener in the manager and starts scan according to the requested scan mode.
+ */
+ @NearbyManager.ScanStatus
+ public int registerScanListener(ScanRequest scanRequest, IScanListener listener,
+ CallerIdentity callerIdentity) {
+ DiscoveryRegistration registration = new DiscoveryRegistration(this, scanRequest, listener,
+ mExecutor, callerIdentity, mMultiplexerLock, mInjector.getAppOpsManager());
+ synchronized (mMultiplexerLock) {
+ putRegistration(listener.asBinder(), registration);
+ return NearbyManager.ScanStatus.SUCCESS;
+ }
+ }
+
+ @Override
+ public void onRegister() {
+ Log.v(TAG, "Registering the DiscoveryProviderManager.");
+ startProviders();
+ }
+
+ @Override
+ public void onUnregister() {
+ Log.v(TAG, "Unregistering the DiscoveryProviderManager.");
+ stopProviders();
+ }
+
+ /**
+ * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
+ */
+ public void unregisterScanListener(IScanListener listener) {
+ Log.v(TAG, "Unregister scan listener");
+ synchronized (mMultiplexerLock) {
+ removeRegistration(listener.asBinder());
+ }
+ // TODO(b/221082271): updates the scan with reduced filters.
+ }
+
+ /**
+ * Query offload capability in a device.
+ */
+ public void queryOffloadCapability(IOffloadCallback callback) {
+ mChreDiscoveryProvider.queryOffloadCapability(callback);
+ }
+
+ /**
+ * @return {@code null} when all providers are initializing
+ * {@code false} when fail to start all the providers
+ * {@code true} when any one of the provider starts successfully
+ */
+ @VisibleForTesting
+ @Nullable
+ Boolean startProviders() {
+ synchronized (mMultiplexerLock) {
+ if (!mMerged.getMediums().contains(MergedDiscoveryRequest.Medium.BLE)) {
+ Log.w(TAG, "failed to start any provider because client disabled BLE");
+ return false;
+ }
+ Set<ScanFilter> scanFilters = mMerged.getScanFilters();
+ boolean chreOnly = isChreOnly(scanFilters);
+ Boolean chreAvailable = mChreDiscoveryProvider.available();
+ Log.v(TAG, "startProviders: chreOnly " + chreOnly + " chreAvailable " + chreAvailable);
+ if (chreAvailable == null) {
+ if (chreOnly) {
+ Log.w(TAG, "client wants CHRE only and Nearby service is still querying CHRE"
+ + " status");
+ return null;
+ }
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ if (!chreAvailable) {
+ if (chreOnly) {
+ Log.w(TAG,
+ "failed to start any provider because client wants CHRE only and CHRE"
+ + " is not available");
+ return false;
+ }
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ if (mMerged.getScanTypes().contains(SCAN_TYPE_NEARBY_PRESENCE)) {
+ startChreProvider(scanFilters);
+ return true;
+ }
+
+ startBleProvider(scanFilters);
+ return true;
+ }
+ }
+
+ @GuardedBy("mMultiplexerLock")
+ private void startBleProvider(Set<ScanFilter> scanFilters) {
+ if (!mBleDiscoveryProvider.getController().isStarted()) {
+ Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+ mBleDiscoveryProvider.getController().setListener(this);
+ mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
+ mBleDiscoveryProvider.getController().setProviderScanFilters(
+ new ArrayList<>(scanFilters));
+ mBleDiscoveryProvider.getController().start();
+ }
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mMultiplexerLock")
+ void startChreProvider(Collection<ScanFilter> scanFilters) {
+ Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning. " + mMerged);
+ mChreDiscoveryProvider.getController().setProviderScanFilters(new ArrayList<>(scanFilters));
+ mChreDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
+ mChreDiscoveryProvider.getController().start();
+ }
+
+ private void stopProviders() {
+ stopBleProvider();
+ stopChreProvider();
+ }
+
+ private void stopBleProvider() {
+ mBleDiscoveryProvider.getController().stop();
+ }
+
+ @VisibleForTesting
+ protected void stopChreProvider() {
+ mChreDiscoveryProvider.getController().stop();
+ }
+
+ @VisibleForTesting
+ void invalidateProviderScanMode() {
+ if (mBleDiscoveryProvider.getController().isStarted()) {
+ synchronized (mMultiplexerLock) {
+ mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
+ }
+ } else {
+ Log.d(TAG, "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
+ + "started.");
+ }
+ }
+
+ @Override
+ public MergedDiscoveryRequest mergeRegistrations(
+ @NonNull Collection<DiscoveryRegistration> registrations) {
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ int scanMode = ScanRequest.SCAN_MODE_NO_POWER;
+ for (DiscoveryRegistration registration : registrations) {
+ builder.addActions(registration.getActions());
+ builder.addScanFilters(registration.getPresenceScanFilters());
+ Log.d(TAG,
+ "mergeRegistrations: type is " + registration.getScanRequest().getScanType());
+ builder.addScanType(registration.getScanRequest().getScanType());
+ if (registration.getScanRequest().isBleEnabled()) {
+ builder.addMedium(MergedDiscoveryRequest.Medium.BLE);
+ }
+ int requestScanMode = registration.getScanRequest().getScanMode();
+ if (scanMode < requestScanMode) {
+ scanMode = requestScanMode;
+ }
+ }
+ builder.setScanMode(scanMode);
+ return builder.build();
+ }
+
+ @Override
+ public void onMergedRegistrationsUpdated() {
+ invalidateProviderScanMode();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
new file mode 100644
index 0000000..4b76eba
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
@@ -0,0 +1,506 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.NearbyManager;
+import android.nearby.PresenceScanFilter;
+import android.nearby.ScanCallback;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.metrics.NearbyMetrics;
+import com.android.server.nearby.presence.PresenceDiscoveryResult;
+import com.android.server.nearby.provider.AbstractDiscoveryProvider;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.provider.PrivacyFilter;
+import com.android.server.nearby.util.identity.CallerIdentity;
+import com.android.server.nearby.util.permissions.DiscoveryPermissions;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/** Manages all aspects of discovery providers. */
+public class DiscoveryProviderManagerLegacy implements AbstractDiscoveryProvider.Listener,
+ DiscoveryManager {
+
+ protected final Object mLock = new Object();
+ @VisibleForTesting
+ @Nullable
+ final ChreDiscoveryProvider mChreDiscoveryProvider;
+ private final Context mContext;
+ private final BleDiscoveryProvider mBleDiscoveryProvider;
+ private final Injector mInjector;
+ @ScanRequest.ScanMode
+ private int mScanMode;
+ @GuardedBy("mLock")
+ private final Map<IBinder, ScanListenerRecord> mScanTypeScanListenerRecordMap;
+
+ public DiscoveryProviderManagerLegacy(Context context, Injector injector) {
+ mContext = context;
+ mBleDiscoveryProvider = new BleDiscoveryProvider(mContext, injector);
+ Executor executor = Executors.newSingleThreadExecutor();
+ mChreDiscoveryProvider =
+ new ChreDiscoveryProvider(
+ mContext, new ChreCommunication(injector, mContext, executor), executor);
+ mScanTypeScanListenerRecordMap = new HashMap<>();
+ mInjector = injector;
+ Log.v(TAG, "DiscoveryProviderManagerLegacy: ");
+ }
+
+ @VisibleForTesting
+ DiscoveryProviderManagerLegacy(Context context, Injector injector,
+ BleDiscoveryProvider bleDiscoveryProvider,
+ ChreDiscoveryProvider chreDiscoveryProvider,
+ Map<IBinder, ScanListenerRecord> scanTypeScanListenerRecordMap) {
+ mContext = context;
+ mInjector = injector;
+ mBleDiscoveryProvider = bleDiscoveryProvider;
+ mChreDiscoveryProvider = chreDiscoveryProvider;
+ mScanTypeScanListenerRecordMap = scanTypeScanListenerRecordMap;
+ }
+
+ private static boolean isChreOnly(List<ScanFilter> scanFilters) {
+ for (ScanFilter scanFilter : scanFilters) {
+ List<DataElement> dataElements =
+ ((PresenceScanFilter) scanFilter).getExtendedProperties();
+ for (DataElement dataElement : dataElements) {
+ if (dataElement.getKey() != DataElement.DataType.SCAN_MODE) {
+ continue;
+ }
+ byte[] scanModeValue = dataElement.getValue();
+ if (scanModeValue == null || scanModeValue.length == 0) {
+ break;
+ }
+ if (Byte.toUnsignedInt(scanModeValue[0]) == ScanRequest.SCAN_MODE_CHRE_ONLY) {
+ return true;
+ }
+ }
+
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ static boolean presenceFilterMatches(
+ NearbyDeviceParcelable device, List<ScanFilter> scanFilters) {
+ if (scanFilters.isEmpty()) {
+ return true;
+ }
+ PresenceDiscoveryResult discoveryResult = PresenceDiscoveryResult.fromDevice(device);
+ for (ScanFilter scanFilter : scanFilters) {
+ PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
+ if (discoveryResult.matches(presenceScanFilter)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
+ synchronized (mLock) {
+ AppOpsManager appOpsManager = Objects.requireNonNull(mInjector.getAppOpsManager());
+ for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
+ ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
+ if (record == null) {
+ Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+ continue;
+ }
+ CallerIdentity callerIdentity = record.getCallerIdentity();
+ if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
+ appOpsManager, callerIdentity)) {
+ Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+ + "- not forwarding results");
+ try {
+ record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
+ } catch (RemoteException e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+ }
+ return;
+ }
+
+ if (nearbyDevice.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
+ List<ScanFilter> presenceFilters =
+ record.getScanRequest().getScanFilters().stream()
+ .filter(
+ scanFilter ->
+ scanFilter.getType()
+ == SCAN_TYPE_NEARBY_PRESENCE)
+ .collect(Collectors.toList());
+ if (!presenceFilterMatches(nearbyDevice, presenceFilters)) {
+ Log.d(TAG, "presence filter does not match for "
+ + "the scanned Presence Device");
+ continue;
+ }
+ }
+ try {
+ record.getScanListener()
+ .onDiscovered(
+ PrivacyFilter.filter(
+ record.getScanRequest().getScanType(), nearbyDevice));
+ NearbyMetrics.logScanDeviceDiscovered(
+ record.hashCode(), record.getScanRequest(), nearbyDevice);
+ } catch (RemoteException e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onError(int errorCode) {
+ synchronized (mLock) {
+ AppOpsManager appOpsManager = Objects.requireNonNull(mInjector.getAppOpsManager());
+ for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
+ ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
+ if (record == null) {
+ Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+ continue;
+ }
+ CallerIdentity callerIdentity = record.getCallerIdentity();
+ if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
+ appOpsManager, callerIdentity)) {
+ Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+ + "- not forwarding results");
+ try {
+ record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
+ } catch (RemoteException e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+ }
+ return;
+ }
+
+ try {
+ record.getScanListener().onError(errorCode);
+ } catch (RemoteException e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report onError.", e);
+ }
+ }
+ }
+ }
+
+ /** Called after boot completed. */
+ public void init() {
+ if (mInjector.getContextHubManager() != null) {
+ mChreDiscoveryProvider.init();
+ }
+ mChreDiscoveryProvider.getController().setListener(this);
+ }
+
+ /**
+ * Registers the listener in the manager and starts scan according to the requested scan mode.
+ */
+ @NearbyManager.ScanStatus
+ public int registerScanListener(ScanRequest scanRequest, IScanListener listener,
+ CallerIdentity callerIdentity) {
+ synchronized (mLock) {
+ ScanListenerDeathRecipient deathRecipient = (listener != null)
+ ? new ScanListenerDeathRecipient(listener) : null;
+ IBinder listenerBinder = listener.asBinder();
+ if (listenerBinder != null && deathRecipient != null) {
+ try {
+ listenerBinder.linkToDeath(deathRecipient, 0);
+ } catch (RemoteException e) {
+ throw new IllegalArgumentException("Can't link to scan listener's death");
+ }
+ }
+ if (mScanTypeScanListenerRecordMap.containsKey(listener.asBinder())) {
+ ScanRequest savedScanRequest =
+ mScanTypeScanListenerRecordMap.get(listenerBinder).getScanRequest();
+ if (scanRequest.equals(savedScanRequest)) {
+ Log.d(TAG, "Already registered the scanRequest: " + scanRequest);
+ return NearbyManager.ScanStatus.SUCCESS;
+ }
+ }
+ ScanListenerRecord scanListenerRecord =
+ new ScanListenerRecord(scanRequest, listener, callerIdentity, deathRecipient);
+
+ mScanTypeScanListenerRecordMap.put(listenerBinder, scanListenerRecord);
+ Boolean started = startProviders(scanRequest);
+ if (started == null) {
+ mScanTypeScanListenerRecordMap.remove(listenerBinder);
+ return NearbyManager.ScanStatus.UNKNOWN;
+ }
+ if (!started) {
+ mScanTypeScanListenerRecordMap.remove(listenerBinder);
+ return NearbyManager.ScanStatus.ERROR;
+ }
+ NearbyMetrics.logScanStarted(scanListenerRecord.hashCode(), scanRequest);
+ if (mScanMode < scanRequest.getScanMode()) {
+ mScanMode = scanRequest.getScanMode();
+ invalidateProviderScanMode();
+ }
+ return NearbyManager.ScanStatus.SUCCESS;
+ }
+ }
+
+ /**
+ * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
+ */
+ public void unregisterScanListener(IScanListener listener) {
+ IBinder listenerBinder = listener.asBinder();
+ synchronized (mLock) {
+ if (!mScanTypeScanListenerRecordMap.containsKey(listenerBinder)) {
+ Log.w(
+ TAG,
+ "Cannot unregister the scanRequest because the request is never "
+ + "registered.");
+ return;
+ }
+
+ ScanListenerRecord removedRecord =
+ mScanTypeScanListenerRecordMap.remove(listenerBinder);
+ ScanListenerDeathRecipient deathRecipient = removedRecord.getDeathRecipient();
+ if (listenerBinder != null && deathRecipient != null) {
+ listenerBinder.unlinkToDeath(removedRecord.getDeathRecipient(), 0);
+ }
+ Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
+ NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
+ if (mScanTypeScanListenerRecordMap.isEmpty()) {
+ Log.v(TAG, "DiscoveryProviderManager stops provider because there is no "
+ + "scan listener registered.");
+ stopProviders();
+ return;
+ }
+
+ // TODO(b/221082271): updates the scan with reduced filters.
+
+ // Removes current highest scan mode requested and sets the next highest scan mode.
+ if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
+ Log.v(TAG, "DiscoveryProviderManager starts to find the new highest scan mode "
+ + "because the highest scan mode listener was unregistered.");
+ @ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
+ // find the next highest scan mode;
+ for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
+ @ScanRequest.ScanMode int scanMode = record.getScanRequest().getScanMode();
+ if (scanMode > highestScanModeRequested) {
+ highestScanModeRequested = scanMode;
+ }
+ }
+ if (mScanMode != highestScanModeRequested) {
+ mScanMode = highestScanModeRequested;
+ invalidateProviderScanMode();
+ }
+ }
+ }
+ }
+
+ /**
+ * Query offload capability in a device.
+ */
+ public void queryOffloadCapability(IOffloadCallback callback) {
+ mChreDiscoveryProvider.queryOffloadCapability(callback);
+ }
+
+ /**
+ * @return {@code null} when all providers are initializing
+ * {@code false} when fail to start all the providers
+ * {@code true} when any one of the provider starts successfully
+ */
+ @VisibleForTesting
+ @Nullable
+ Boolean startProviders(ScanRequest scanRequest) {
+ if (!scanRequest.isBleEnabled()) {
+ Log.w(TAG, "failed to start any provider because client disabled BLE");
+ return false;
+ }
+ List<ScanFilter> scanFilters = getPresenceScanFilters();
+ boolean chreOnly = isChreOnly(scanFilters);
+ Boolean chreAvailable = mChreDiscoveryProvider.available();
+ if (chreAvailable == null) {
+ if (chreOnly) {
+ Log.w(TAG, "client wants CHRE only and Nearby service is still querying CHRE"
+ + " status");
+ return null;
+ }
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ if (!chreAvailable) {
+ if (chreOnly) {
+ Log.w(TAG, "failed to start any provider because client wants CHRE only and CHRE"
+ + " is not available");
+ return false;
+ }
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ if (scanRequest.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
+ startChreProvider(scanFilters);
+ return true;
+ }
+
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ private void startBleProvider(List<ScanFilter> scanFilters) {
+ if (!mBleDiscoveryProvider.getController().isStarted()) {
+ Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+ mBleDiscoveryProvider.getController().setListener(this);
+ mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ mBleDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
+ mBleDiscoveryProvider.getController().start();
+ }
+ }
+
+ @VisibleForTesting
+ void startChreProvider(List<ScanFilter> scanFilters) {
+ Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
+ mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
+ mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ mChreDiscoveryProvider.getController().start();
+ }
+
+ private List<ScanFilter> getPresenceScanFilters() {
+ synchronized (mLock) {
+ List<ScanFilter> scanFilters = new ArrayList();
+ for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
+ ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
+ List<ScanFilter> presenceFilters =
+ record.getScanRequest().getScanFilters().stream()
+ .filter(
+ scanFilter ->
+ scanFilter.getType() == SCAN_TYPE_NEARBY_PRESENCE)
+ .collect(Collectors.toList());
+ scanFilters.addAll(presenceFilters);
+ }
+ return scanFilters;
+ }
+ }
+
+ private void stopProviders() {
+ stopBleProvider();
+ stopChreProvider();
+ }
+
+ private void stopBleProvider() {
+ mBleDiscoveryProvider.getController().stop();
+ }
+
+ @VisibleForTesting
+ protected void stopChreProvider() {
+ mChreDiscoveryProvider.getController().stop();
+ }
+
+ @VisibleForTesting
+ void invalidateProviderScanMode() {
+ if (mBleDiscoveryProvider.getController().isStarted()) {
+ mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ } else {
+ Log.d(
+ TAG,
+ "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
+ + "started.");
+ }
+ }
+
+ @VisibleForTesting
+ static class ScanListenerRecord {
+
+ private final ScanRequest mScanRequest;
+
+ private final IScanListener mScanListener;
+
+ private final CallerIdentity mCallerIdentity;
+
+ private final ScanListenerDeathRecipient mDeathRecipient;
+
+ ScanListenerRecord(ScanRequest scanRequest, IScanListener iScanListener,
+ CallerIdentity callerIdentity, ScanListenerDeathRecipient deathRecipient) {
+ mScanListener = iScanListener;
+ mScanRequest = scanRequest;
+ mCallerIdentity = callerIdentity;
+ mDeathRecipient = deathRecipient;
+ }
+
+ IScanListener getScanListener() {
+ return mScanListener;
+ }
+
+ ScanRequest getScanRequest() {
+ return mScanRequest;
+ }
+
+ CallerIdentity getCallerIdentity() {
+ return mCallerIdentity;
+ }
+
+ ScanListenerDeathRecipient getDeathRecipient() {
+ return mDeathRecipient;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ScanListenerRecord) {
+ ScanListenerRecord otherScanListenerRecord = (ScanListenerRecord) other;
+ return Objects.equals(mScanRequest, otherScanListenerRecord.mScanRequest)
+ && Objects.equals(mScanListener, otherScanListenerRecord.mScanListener);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mScanListener, mScanRequest);
+ }
+ }
+
+ /**
+ * Class to make listener unregister after the binder is dead.
+ */
+ public class ScanListenerDeathRecipient implements IBinder.DeathRecipient {
+ public IScanListener listener;
+
+ ScanListenerDeathRecipient(IScanListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void binderDied() {
+ Log.d(TAG, "Binder is dead - unregistering scan listener");
+ unregisterScanListener(listener);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/ListenerMultiplexer.java b/nearby/service/java/com/android/server/nearby/managers/ListenerMultiplexer.java
new file mode 100644
index 0000000..a6a9388
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/ListenerMultiplexer.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.managers;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.NonNull;
+import android.os.IBinder;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.nearby.managers.registration.BinderListenerRegistration;
+import com.android.server.nearby.managers.registration.BinderListenerRegistration.ListenerOperation;
+
+import java.util.Collection;
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * A simplified class based on {@link com.android.server.location.listeners.ListenerMultiplexer}.
+ * It is a base class to multiplex broadcast and discovery events to multiple listener
+ * registrations. Every listener is represented by a registration object which stores all required
+ * state for a listener.
+ * Registrations will be merged to one request for the service to operate.
+ *
+ * @param <TListener> callback type for clients
+ * @param <TRegistration> child of {@link BinderListenerRegistration}
+ * @param <TMergedRegistration> merged registration type
+ */
+public abstract class ListenerMultiplexer<TListener,
+ TRegistration extends BinderListenerRegistration<TListener>, TMergedRegistration> {
+
+ /**
+ * The lock object used by the multiplexer. Acquiring this lock allows for multiple operations
+ * on the multiplexer to be completed atomically. Otherwise, it is not required to hold this
+ * lock. This lock is held while invoking all lifecycle callbacks on both the multiplexer and
+ * any registrations.
+ */
+ public final Object mMultiplexerLock = new Object();
+
+ @GuardedBy("mMultiplexerLock")
+ final ArrayMap<IBinder, TRegistration> mRegistrations = new ArrayMap<>();
+
+ // this is really @NonNull in many ways, but we explicitly null this out to allow for GC when
+ // not
+ // in use, so we can't annotate with @NonNull
+ @GuardedBy("mMultiplexerLock")
+ public TMergedRegistration mMerged;
+
+ /**
+ * Invoked when the multiplexer goes from having no registrations to having some registrations.
+ * This is a convenient entry point for registering listeners, etc, which only need to be
+ * present
+ * while there are any registrations. Invoked while holding the multiplexer's internal lock.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public void onRegister() {
+ Log.v(TAG, "ListenerMultiplexer registered.");
+ }
+
+ /**
+ * Invoked when the multiplexer goes from having some registrations to having no registrations.
+ * This is a convenient entry point for unregistering listeners, etc, which only need to be
+ * present while there are any registrations. Invoked while holding the multiplexer's internal
+ * lock.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public void onUnregister() {
+ Log.v(TAG, "ListenerMultiplexer unregistered.");
+ }
+
+ /**
+ * Puts a new registration with the given key, replacing any previous registration under the
+ * same key. This method cannot be called to put a registration re-entrantly.
+ */
+ public final void putRegistration(@NonNull IBinder key, @NonNull TRegistration registration) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(registration);
+ synchronized (mMultiplexerLock) {
+ boolean wasEmpty = mRegistrations.isEmpty();
+
+ int index = mRegistrations.indexOfKey(key);
+ if (index > 0) {
+ BinderListenerRegistration<TListener> oldRegistration = mRegistrations.valueAt(
+ index);
+ oldRegistration.onUnregister();
+ mRegistrations.setValueAt(index, registration);
+ } else {
+ mRegistrations.put(key, registration);
+ }
+
+ registration.onRegister();
+ onRegistrationsUpdated();
+ if (wasEmpty) {
+ onRegister();
+ }
+ }
+ }
+
+ /**
+ * Removes the registration with the given key.
+ */
+ public final void removeRegistration(IBinder key) {
+ synchronized (mMultiplexerLock) {
+ int index = mRegistrations.indexOfKey(key);
+ if (index < 0) {
+ return;
+ }
+
+ removeRegistration(index);
+ }
+ }
+
+ @GuardedBy("mMultiplexerLock")
+ private void removeRegistration(int index) {
+ TRegistration registration = mRegistrations.valueAt(index);
+
+ registration.onUnregister();
+ mRegistrations.removeAt(index);
+
+ onRegistrationsUpdated();
+
+ if (mRegistrations.isEmpty()) {
+ onUnregister();
+ }
+ }
+
+ /**
+ * Invoked when a registration is added, removed, or replaced. Invoked while holding the
+ * multiplexer's internal lock.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public final void onRegistrationsUpdated() {
+ TMergedRegistration newMerged = mergeRegistrations(mRegistrations.values());
+ if (newMerged.equals(mMerged)) {
+ return;
+ }
+ mMerged = newMerged;
+ onMergedRegistrationsUpdated();
+ }
+
+ /**
+ * Called in order to generate a merged registration from the given set of active registrations.
+ * The list of registrations will never be empty. If the resulting merged registration is equal
+ * to the currently registered merged registration, nothing further will happen. If the merged
+ * registration differs,{@link #onMergedRegistrationsUpdated()} will be invoked with the new
+ * merged registration so that the backing service can be updated.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public abstract TMergedRegistration mergeRegistrations(
+ @NonNull Collection<TRegistration> registrations);
+
+ /**
+ * The operation that the manager wants to handle when there is an update for the merged
+ * registration.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public abstract void onMergedRegistrationsUpdated();
+
+ protected final void deliverToListeners(
+ Function<TRegistration, ListenerOperation<TListener>> function) {
+ synchronized (mMultiplexerLock) {
+ final int size = mRegistrations.size();
+ for (int i = 0; i < size; i++) {
+ TRegistration registration = mRegistrations.valueAt(i);
+ BinderListenerRegistration.ListenerOperation<TListener> operation = function.apply(
+ registration);
+ if (operation != null) {
+ registration.executeOperation(operation);
+ }
+ }
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/MergedDiscoveryRequest.java b/nearby/service/java/com/android/server/nearby/managers/MergedDiscoveryRequest.java
new file mode 100644
index 0000000..dcfb602
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/MergedDiscoveryRequest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.managers;
+
+import android.annotation.IntDef;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.util.ArraySet;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Collection;
+import java.util.Set;
+
+/** Internal discovery request to {@link DiscoveryProviderManager} and providers */
+public class MergedDiscoveryRequest {
+
+ private static final MergedDiscoveryRequest EMPTY_REQUEST = new MergedDiscoveryRequest(
+ /* scanMode= */ ScanRequest.SCAN_MODE_NO_POWER,
+ /* scanTypes= */ ImmutableSet.of(),
+ /* actions= */ ImmutableSet.of(),
+ /* scanFilters= */ ImmutableSet.of(),
+ /* mediums= */ ImmutableSet.of());
+ @ScanRequest.ScanMode
+ private final int mScanMode;
+ private final Set<Integer> mScanTypes;
+ private final Set<Integer> mActions;
+ private final Set<ScanFilter> mScanFilters;
+ private final Set<Integer> mMediums;
+
+ private MergedDiscoveryRequest(@ScanRequest.ScanMode int scanMode, Set<Integer> scanTypes,
+ Set<Integer> actions, Set<ScanFilter> scanFilters, Set<Integer> mediums) {
+ mScanMode = scanMode;
+ mScanTypes = scanTypes;
+ mActions = actions;
+ mScanFilters = scanFilters;
+ mMediums = mediums;
+ }
+
+ /**
+ * Returns an empty discovery request.
+ *
+ * <p>The empty request is used as the default request when the discovery engine is enabled,
+ * but
+ * there is no request yet. It's also used to notify the discovery engine all clients have
+ * removed
+ * their requests.
+ */
+ public static MergedDiscoveryRequest empty() {
+ return EMPTY_REQUEST;
+ }
+
+ /** Returns the priority of the request */
+ @ScanRequest.ScanMode
+ public final int getScanMode() {
+ return mScanMode;
+ }
+
+ /** Returns all requested scan types. */
+ public ImmutableSet<Integer> getScanTypes() {
+ return ImmutableSet.copyOf(mScanTypes);
+ }
+
+ /** Returns the actions of the request */
+ public ImmutableSet<Integer> getActions() {
+ return ImmutableSet.copyOf(mActions);
+ }
+
+ /** Returns the scan filters of the request */
+ public ImmutableSet<ScanFilter> getScanFilters() {
+ return ImmutableSet.copyOf(mScanFilters);
+ }
+
+ /** Returns the enabled scan mediums */
+ public ImmutableSet<Integer> getMediums() {
+ return ImmutableSet.copyOf(mMediums);
+ }
+
+ /**
+ * The medium where the broadcast request should be sent.
+ *
+ * @hide
+ */
+ @IntDef({Medium.BLE})
+ public @interface Medium {
+ int BLE = 1;
+ }
+
+ /** Builder for {@link MergedDiscoveryRequest}. */
+ public static class Builder {
+ private final Set<Integer> mScanTypes;
+ private final Set<Integer> mActions;
+ private final Set<ScanFilter> mScanFilters;
+ private final Set<Integer> mMediums;
+ @ScanRequest.ScanMode
+ private int mScanMode;
+
+ public Builder() {
+ mScanMode = ScanRequest.SCAN_MODE_NO_POWER;
+ mScanTypes = new ArraySet<>();
+ mActions = new ArraySet<>();
+ mScanFilters = new ArraySet<>();
+ mMediums = new ArraySet<>();
+ }
+
+ /**
+ * Sets the priority for the engine request.
+ */
+ public Builder setScanMode(@ScanRequest.ScanMode int scanMode) {
+ mScanMode = scanMode;
+ return this;
+ }
+
+ /**
+ * Adds scan type to the request.
+ */
+ public Builder addScanType(@ScanRequest.ScanType int type) {
+ mScanTypes.add(type);
+ return this;
+ }
+
+ /** Add actions to the request. */
+ public Builder addActions(Collection<Integer> actions) {
+ mActions.addAll(actions);
+ return this;
+ }
+
+ /** Add actions to the request. */
+ public Builder addScanFilters(Collection<ScanFilter> scanFilters) {
+ mScanFilters.addAll(scanFilters);
+ return this;
+ }
+
+ /**
+ * Add mediums to the request.
+ */
+ public Builder addMedium(@Medium int medium) {
+ mMediums.add(medium);
+ return this;
+ }
+
+ /** Builds an instance of {@link MergedDiscoveryRequest}. */
+ public MergedDiscoveryRequest build() {
+ return new MergedDiscoveryRequest(mScanMode, mScanTypes, mActions, mScanFilters,
+ mMediums);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/registration/BinderListenerRegistration.java b/nearby/service/java/com/android/server/nearby/managers/registration/BinderListenerRegistration.java
new file mode 100644
index 0000000..4aaa08f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/registration/BinderListenerRegistration.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.managers.registration;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.server.nearby.managers.ListenerMultiplexer;
+
+import java.util.NoSuchElementException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A listener registration object which holds data associated with the listener, such as an optional
+ * request, and an executor responsible for listener invocations. Key is the IBinder.
+ *
+ * @param <TListener> listener for the callback
+ */
+public abstract class BinderListenerRegistration<TListener> implements IBinder.DeathRecipient {
+
+ private final AtomicBoolean mRemoved = new AtomicBoolean(false);
+ private final Executor mExecutor;
+ private final Object mListenerLock = new Object();
+ @Nullable
+ TListener mListener;
+ @Nullable
+ private final IBinder mKey;
+
+ public BinderListenerRegistration(IBinder key, Executor executor, TListener listener) {
+ this.mKey = key;
+ this.mExecutor = executor;
+ this.mListener = listener;
+ }
+
+ /**
+ * Must be implemented to return the
+ * {@link com.android.server.nearby.managers.ListenerMultiplexer} this registration is
+ * registered
+ * with. Often this is easiest to accomplish by defining registration subclasses as non-static
+ * inner classes of the multiplexer they are to be used with.
+ */
+ public abstract ListenerMultiplexer<TListener, ?
+ extends BinderListenerRegistration<TListener>, ?> getOwner();
+
+ public final IBinder getBinder() {
+ return mKey;
+ }
+
+ public final Executor getExecutor() {
+ return mExecutor;
+ }
+
+ /**
+ * Called when the registration is put in the Multiplexer.
+ */
+ public void onRegister() {
+ try {
+ getBinder().linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ remove();
+ }
+ }
+
+ /**
+ * Called when the registration is removed in the Multiplexer.
+ */
+ public void onUnregister() {
+ this.mListener = null;
+ try {
+ getBinder().unlinkToDeath(this, 0);
+ } catch (NoSuchElementException e) {
+ Log.w(TAG, "failed to unregister binder death listener", e);
+ }
+ }
+
+ /**
+ * Removes this registration. All pending listener invocations will fail.
+ *
+ * <p>Does nothing if invoked before {@link #onRegister()} or after {@link #onUnregister()}.
+ */
+ public final void remove() {
+ IBinder key = mKey;
+ if (key != null && !mRemoved.getAndSet(true)) {
+ getOwner().removeRegistration(key);
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ remove();
+ }
+
+ /**
+ * May be overridden by subclasses to handle listener operation failures. The default behavior
+ * is
+ * to further propagate any exceptions. Will always be invoked on the executor thread.
+ */
+ protected void onOperationFailure(Exception exception) {
+ throw new AssertionError(exception);
+ }
+
+ /**
+ * Executes the given listener operation on the registration executor, invoking {@link
+ * #onOperationFailure(Exception)} in case the listener operation fails. If the registration is
+ * removed prior to the operation running, the operation is considered canceled. If a null
+ * operation is supplied, nothing happens.
+ */
+ public final void executeOperation(@Nullable ListenerOperation<TListener> operation) {
+ if (operation == null) {
+ return;
+ }
+
+ synchronized (mListenerLock) {
+ if (mListener == null) {
+ return;
+ }
+
+ AtomicBoolean complete = new AtomicBoolean(false);
+ mExecutor.execute(() -> {
+ TListener listener;
+ synchronized (mListenerLock) {
+ listener = mListener;
+ }
+
+ Exception failure = null;
+ if (listener != null) {
+ try {
+ operation.operate(listener);
+ } catch (Exception e) {
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ } else {
+ failure = e;
+ }
+ }
+ }
+
+ operation.onComplete(failure == null);
+ complete.set(true);
+
+ if (failure != null) {
+ onOperationFailure(failure);
+ }
+ });
+ operation.onScheduled(complete.get());
+ }
+ }
+
+ /**
+ * An listener operation to perform.
+ *
+ * @param <ListenerT> listener type
+ */
+ public interface ListenerOperation<ListenerT> {
+
+ /**
+ * Invoked after the operation has been scheduled for execution. The {@code complete}
+ * argument
+ * will be true if {@link #onComplete(boolean)} was invoked prior to this callback (such as
+ * if
+ * using a direct executor), or false if {@link #onComplete(boolean)} will be invoked after
+ * this
+ * callback. This method is always invoked on the calling thread.
+ */
+ default void onScheduled(boolean complete) {
+ }
+
+ /**
+ * Invoked to perform an operation on the given listener. This method is always invoked on
+ * the
+ * executor thread. If this method throws a checked exception, the operation will fail and
+ * result in {@link #onOperationFailure(Exception)} being invoked. If this method throws an
+ * unchecked exception, this propagates normally and should result in a crash.
+ */
+ void operate(ListenerT listener) throws Exception;
+
+ /**
+ * Invoked after the operation is complete. The {@code success} argument will be true if
+ * the
+ * operation completed without throwing any exceptions, and false otherwise (such as if the
+ * operation was canceled prior to executing, or if it threw an exception). This invocation
+ * may
+ * happen either before or after (but never during) the invocation of {@link
+ * #onScheduled(boolean)}. This method is always invoked on the executor thread.
+ */
+ default void onComplete(boolean success) {
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/registration/DiscoveryRegistration.java b/nearby/service/java/com/android/server/nearby/managers/registration/DiscoveryRegistration.java
new file mode 100644
index 0000000..91237d2
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/registration/DiscoveryRegistration.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.managers.registration;
+
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.ScanCallback;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.common.CancelableAlarm;
+import com.android.server.nearby.managers.ListenerMultiplexer;
+import com.android.server.nearby.managers.MergedDiscoveryRequest;
+import com.android.server.nearby.presence.PresenceDiscoveryResult;
+import com.android.server.nearby.util.identity.CallerIdentity;
+import com.android.server.nearby.util.permissions.DiscoveryPermissions;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.stream.Collectors;
+
+/**
+ * Class responsible for all client based operations. Each {@link DiscoveryRegistration} is for one
+ * valid unique {@link android.nearby.NearbyManager#startScan(ScanRequest, Executor, ScanCallback)}
+ */
+public class DiscoveryRegistration extends BinderListenerRegistration<IScanListener> {
+
+ /**
+ * Timeout before a previous discovered device is reported as lost.
+ */
+ @VisibleForTesting
+ static final int ON_LOST_TIME_OUT_MS = 10000;
+ /** Lock for registration operations. */
+ final Object mMultiplexerLock;
+ private final ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest>
+ mOwner;
+ private final AppOpsManager mAppOpsManager;
+ /** Presence devices that are currently discovered, and not lost yet. */
+ @GuardedBy("mMultiplexerLock")
+ private final Map<Long, NearbyDeviceParcelable> mDiscoveredDevices;
+ /** A map of deviceId and alarms for reporting device lost. */
+ @GuardedBy("mMultiplexerLock")
+ private final Map<Long, DeviceOnLostAlarm> mDiscoveryOnLostAlarmPerDevice = new ArrayMap<>();
+ /**
+ * The single thread executor to run {@link CancelableAlarm} to report
+ * {@link NearbyDeviceParcelable} on lost after timeout.
+ */
+ private final ScheduledExecutorService mAlarmExecutor =
+ Executors.newSingleThreadScheduledExecutor();
+ private final ScanRequest mScanRequest;
+ private final CallerIdentity mCallerIdentity;
+
+ public DiscoveryRegistration(
+ ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> owner,
+ ScanRequest scanRequest, IScanListener scanListener, Executor executor,
+ CallerIdentity callerIdentity, Object multiplexerLock, AppOpsManager appOpsManager) {
+ super(scanListener.asBinder(), executor, scanListener);
+ mOwner = owner;
+ mListener = scanListener;
+ mScanRequest = scanRequest;
+ mCallerIdentity = callerIdentity;
+ mMultiplexerLock = multiplexerLock;
+ mDiscoveredDevices = new ArrayMap<>();
+ mAppOpsManager = appOpsManager;
+ }
+
+ /**
+ * Gets the scan request.
+ */
+ public ScanRequest getScanRequest() {
+ return mScanRequest;
+ }
+
+ /**
+ * Gets the actions from the scan filter(s).
+ */
+ public Set<Integer> getActions() {
+ Set<Integer> result = new ArraySet<>();
+ List<ScanFilter> filters = mScanRequest.getScanFilters();
+ for (ScanFilter filter : filters) {
+ if (filter instanceof PresenceScanFilter) {
+ result.addAll(((PresenceScanFilter) filter).getPresenceActions());
+ }
+ }
+ return ImmutableSet.copyOf(result);
+ }
+
+ /**
+ * Gets all the filters that are for Nearby Presence.
+ */
+ public Set<ScanFilter> getPresenceScanFilters() {
+ Set<ScanFilter> result = new ArraySet<>();
+ List<ScanFilter> filters = mScanRequest.getScanFilters();
+ for (ScanFilter filter : filters) {
+ if (filter.getType() == SCAN_TYPE_NEARBY_PRESENCE) {
+ result.add(filter);
+ }
+ }
+ return ImmutableSet.copyOf(result);
+ }
+
+ @VisibleForTesting
+ Map<Long, DeviceOnLostAlarm> getDiscoveryOnLostAlarms() {
+ synchronized (mMultiplexerLock) {
+ return mDiscoveryOnLostAlarmPerDevice;
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof DiscoveryRegistration) {
+ DiscoveryRegistration otherRegistration = (DiscoveryRegistration) other;
+ return Objects.equals(mScanRequest, otherRegistration.mScanRequest) && Objects.equals(
+ mListener, otherRegistration.mListener);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mListener, mScanRequest);
+ }
+
+ @Override
+ public ListenerMultiplexer<
+ IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> getOwner() {
+ return mOwner;
+ }
+
+ @VisibleForTesting
+ ListenerOperation<IScanListener> reportDeviceLost(NearbyDeviceParcelable device) {
+ long deviceId = device.getDeviceId();
+ return reportResult(DiscoveryResult.DEVICE_LOST, device, () -> {
+ synchronized (mMultiplexerLock) {
+ // Remove the device from reporting devices after reporting lost.
+ mDiscoveredDevices.remove(deviceId);
+ DeviceOnLostAlarm alarm = mDiscoveryOnLostAlarmPerDevice.remove(deviceId);
+ if (alarm != null) {
+ alarm.cancel();
+ }
+ }
+ });
+ }
+
+ /**
+ * Called when there is device discovered from the server.
+ */
+ public ListenerOperation<IScanListener> onNearbyDeviceDiscovered(
+ NearbyDeviceParcelable device) {
+ if (!filterCheck(device)) {
+ Log.d(TAG, "presence filter does not match for the scanned Presence Device");
+ return null;
+ }
+ synchronized (mMultiplexerLock) {
+ long deviceId = device.getDeviceId();
+ boolean deviceReported = mDiscoveredDevices.containsKey(deviceId);
+ scheduleOnLostAlarm(device);
+ if (deviceReported) {
+ NearbyDeviceParcelable oldDevice = mDiscoveredDevices.get(deviceId);
+ if (device.equals(oldDevice)) {
+ return null;
+ }
+ return reportUpdated(device);
+ }
+ return reportDiscovered(device);
+ }
+ }
+
+ @VisibleForTesting
+ static boolean presenceFilterMatches(NearbyDeviceParcelable device,
+ List<ScanFilter> scanFilters) {
+ if (scanFilters.isEmpty()) {
+ return true;
+ }
+ PresenceDiscoveryResult discoveryResult = PresenceDiscoveryResult.fromDevice(device);
+ for (ScanFilter scanFilter : scanFilters) {
+ PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
+ if (discoveryResult.matches(presenceScanFilter)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ ListenerOperation<IScanListener> reportDiscovered(NearbyDeviceParcelable device) {
+ long deviceId = device.getDeviceId();
+ return reportResult(DiscoveryResult.DEVICE_DISCOVERED, device, () -> {
+ synchronized (mMultiplexerLock) {
+ // Add the device to discovered devices after reporting device is
+ // discovered.
+ mDiscoveredDevices.put(deviceId, device);
+ scheduleOnLostAlarm(device);
+ }
+ });
+ }
+
+ @Nullable
+ ListenerOperation<IScanListener> reportUpdated(NearbyDeviceParcelable device) {
+ long deviceId = device.getDeviceId();
+ return reportResult(DiscoveryResult.DEVICE_UPDATED, device, () -> {
+ synchronized (mMultiplexerLock) {
+ // Update the new device to discovered devices after reporting device is
+ // discovered.
+ mDiscoveredDevices.put(deviceId, device);
+ scheduleOnLostAlarm(device);
+ }
+ });
+
+ }
+
+ /** Reports an error to the client. */
+ public ListenerOperation<IScanListener> reportError(@ScanCallback.ErrorCode int errorCode) {
+ return listener -> listener.onError(errorCode);
+ }
+
+ @Nullable
+ ListenerOperation<IScanListener> reportResult(@DiscoveryResult int result,
+ NearbyDeviceParcelable device, @Nullable Runnable successReportCallback) {
+ // Report the operation to AppOps.
+ // NOTE: AppOps report has to be the last operation before delivering the result. Otherwise
+ // we may over-report when the discovery result doesn't end up being delivered.
+ if (!checkIdentity()) {
+ return reportError(ScanCallback.ERROR_PERMISSION_DENIED);
+ }
+
+ return new ListenerOperation<>() {
+
+ @Override
+ public void operate(IScanListener listener) throws Exception {
+ switch (result) {
+ case DiscoveryResult.DEVICE_DISCOVERED:
+ listener.onDiscovered(device);
+ break;
+ case DiscoveryResult.DEVICE_UPDATED:
+ listener.onUpdated(device);
+ break;
+ case DiscoveryResult.DEVICE_LOST:
+ listener.onLost(device);
+ break;
+ }
+ }
+
+ @Override
+ public void onComplete(boolean success) {
+ if (success) {
+ if (successReportCallback != null) {
+ successReportCallback.run();
+ Log.d(TAG, "Successfully delivered result to caller.");
+ }
+ }
+ }
+ };
+ }
+
+ private boolean filterCheck(NearbyDeviceParcelable device) {
+ if (device.getScanType() != SCAN_TYPE_NEARBY_PRESENCE) {
+ return true;
+ }
+ List<ScanFilter> presenceFilters = mScanRequest.getScanFilters().stream().filter(
+ scanFilter -> scanFilter.getType() == SCAN_TYPE_NEARBY_PRESENCE).collect(
+ Collectors.toList());
+ return presenceFilterMatches(device, presenceFilters);
+ }
+
+ private boolean checkIdentity() {
+ boolean result = DiscoveryPermissions.noteDiscoveryResultDelivery(mAppOpsManager,
+ mCallerIdentity);
+ if (!result) {
+ Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+ + "- not forwarding results for the registration.");
+ }
+ return result;
+ }
+
+ @GuardedBy("mMultiplexerLock")
+ private void scheduleOnLostAlarm(NearbyDeviceParcelable device) {
+ long deviceId = device.getDeviceId();
+ DeviceOnLostAlarm alarm = mDiscoveryOnLostAlarmPerDevice.get(deviceId);
+ if (alarm == null) {
+ alarm = new DeviceOnLostAlarm(device, mAlarmExecutor);
+ mDiscoveryOnLostAlarmPerDevice.put(deviceId, alarm);
+ }
+ alarm.start();
+ Log.d(TAG, "DiscoveryProviderManager updated state for " + device.getDeviceId());
+ }
+
+ /** Status of the discovery result. */
+ @IntDef({DiscoveryResult.DEVICE_DISCOVERED, DiscoveryResult.DEVICE_UPDATED,
+ DiscoveryResult.DEVICE_LOST})
+ public @interface DiscoveryResult {
+ int DEVICE_DISCOVERED = 0;
+ int DEVICE_UPDATED = 1;
+ int DEVICE_LOST = 2;
+ }
+
+ private class DeviceOnLostAlarm {
+
+ private static final String NAME = "DeviceOnLostAlarm";
+ private final NearbyDeviceParcelable mDevice;
+ private final ScheduledExecutorService mAlarmExecutor;
+ @Nullable
+ private CancelableAlarm mTimeoutAlarm;
+
+ DeviceOnLostAlarm(NearbyDeviceParcelable device, ScheduledExecutorService alarmExecutor) {
+ mDevice = device;
+ mAlarmExecutor = alarmExecutor;
+ }
+
+ synchronized void start() {
+ cancel();
+ this.mTimeoutAlarm = CancelableAlarm.createSingleAlarm(NAME, () -> {
+ Log.d(TAG, String.format("%s timed out after %d ms. Reporting %s on lost.", NAME,
+ ON_LOST_TIME_OUT_MS, mDevice.getName()));
+ synchronized (mMultiplexerLock) {
+ executeOperation(reportDeviceLost(mDevice));
+ }
+ }, ON_LOST_TIME_OUT_MS, mAlarmExecutor);
+ }
+
+ synchronized void cancel() {
+ if (mTimeoutAlarm != null) {
+ mTimeoutAlarm.cancel();
+ mTimeoutAlarm = null;
+ }
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/Advertisement.java b/nearby/service/java/com/android/server/nearby/presence/Advertisement.java
new file mode 100644
index 0000000..d42f6c7
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/Advertisement.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.presence;
+
+import android.annotation.Nullable;
+import android.nearby.BroadcastRequest;
+import android.nearby.PresenceCredential;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** A Nearby Presence advertisement to be advertised. */
+public abstract class Advertisement {
+
+ @BroadcastRequest.BroadcastVersion
+ int mVersion = BroadcastRequest.PRESENCE_VERSION_UNKNOWN;
+ int mLength;
+ @PresenceCredential.IdentityType int mIdentityType;
+ byte[] mIdentity;
+ byte[] mSalt;
+ List<Integer> mActions;
+
+ /** Serialize an {@link Advertisement} object into bytes. */
+ @Nullable
+ public byte[] toBytes() {
+ return new byte[0];
+ }
+
+ /** Returns the length of the advertisement. */
+ public int getLength() {
+ return mLength;
+ }
+
+ /** Returns the version in the advertisement. */
+ @BroadcastRequest.BroadcastVersion
+ public int getVersion() {
+ return mVersion;
+ }
+
+ /** Returns the identity type in the advertisement. */
+ @PresenceCredential.IdentityType
+ public int getIdentityType() {
+ return mIdentityType;
+ }
+
+ /** Returns the identity bytes in the advertisement. */
+ public byte[] getIdentity() {
+ return mIdentity.clone();
+ }
+
+ /** Returns the salt of the advertisement. */
+ public byte[] getSalt() {
+ return mSalt.clone();
+ }
+
+ /** Returns the actions in the advertisement. */
+ public List<Integer> getActions() {
+ return new ArrayList<>(mActions);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/DataElementHeader.java b/nearby/service/java/com/android/server/nearby/presence/DataElementHeader.java
new file mode 100644
index 0000000..ae4a728
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/DataElementHeader.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.presence;
+
+import android.annotation.Nullable;
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Represents a data element header in Nearby Presence.
+ * Each header has 3 parts: tag, length and style.
+ * Tag: 1 bit (MSB at each byte). 1 for extending, which means there will be more bytes after
+ * the current one for the header.
+ * Length: The total length of a Data Element field. Length is up to 127 and is limited within
+ * the entire first byte in the header. (7 bits, MSB is the tag).
+ * Type: Represents {@link DataElement.DataType}. There is no limit for the type number.
+ *
+ * @hide
+ */
+public class DataElementHeader {
+ // Each Data reserved MSB for tag.
+ static final int TAG_BITMASK = 0b10000000;
+ static final int TAG_OFFSET = 7;
+
+ // If the header is only 1 byte, it has the format: 0b0LLLTTTT. (L for length, T for type.)
+ static final int SINGLE_AVAILABLE_LENGTH_BIT = 3;
+ static final int SINGLE_AVAILABLE_TYPE_BIT = 4;
+ static final int SINGLE_LENGTH_BITMASK = 0b01110000;
+ static final int SINGLE_LENGTH_OFFSET = SINGLE_AVAILABLE_TYPE_BIT;
+ static final int SINGLE_TYPE_BITMASK = 0b00001111;
+
+ // If there are multiple data element headers.
+ // First byte is always the length.
+ static final int MULTIPLE_LENGTH_BYTE = 1;
+ // Each byte reserves MSB for tag.
+ static final int MULTIPLE_BITMASK = 0b01111111;
+
+ @BroadcastRequest.BroadcastVersion
+ private final int mVersion;
+ @DataElement.DataType
+ private final int mDataType;
+ private final int mDataLength;
+
+ DataElementHeader(@BroadcastRequest.BroadcastVersion int version,
+ @DataElement.DataType int dataType, int dataLength) {
+ Preconditions.checkArgument(version == BroadcastRequest.PRESENCE_VERSION_V1,
+ "DataElementHeader is only supported in V1.");
+ Preconditions.checkArgument(dataLength >= 0, "Length should not be negative.");
+ Preconditions.checkArgument(dataLength < (1 << TAG_OFFSET),
+ "Data element should be equal or shorter than 128.");
+
+ this.mVersion = version;
+ this.mDataType = dataType;
+ this.mDataLength = dataLength;
+ }
+
+ /**
+ * The total type of the data element.
+ */
+ @DataElement.DataType
+ public int getDataType() {
+ return mDataType;
+ }
+
+ /**
+ * The total length of a Data Element field.
+ */
+ public int getDataLength() {
+ return mDataLength;
+ }
+
+ /** Serialize a {@link DataElementHeader} object into bytes. */
+ public byte[] toBytes() {
+ Preconditions.checkState(mVersion == BroadcastRequest.PRESENCE_VERSION_V1,
+ "DataElementHeader is only supported in V1.");
+ // Only 1 byte needed for the header
+ if (mDataType < (1 << SINGLE_AVAILABLE_TYPE_BIT)
+ && mDataLength < (1 << SINGLE_AVAILABLE_LENGTH_BIT)) {
+ return new byte[]{createSingleByteHeader(mDataType, mDataLength)};
+ }
+
+ return createMultipleBytesHeader(mDataType, mDataLength);
+ }
+
+ /** Creates a {@link DataElementHeader} object from bytes. */
+ @Nullable
+ public static DataElementHeader fromBytes(@BroadcastRequest.BroadcastVersion int version,
+ @Nonnull byte[] bytes) {
+ Objects.requireNonNull(bytes, "Data parsed in for DataElement should not be null.");
+
+ if (bytes.length == 0) {
+ return null;
+ }
+
+ if (bytes.length == 1) {
+ if (isExtending(bytes[0])) {
+ throw new IllegalArgumentException("The header is not complete.");
+ }
+ return new DataElementHeader(BroadcastRequest.PRESENCE_VERSION_V1,
+ getTypeSingleByte(bytes[0]), getLengthSingleByte(bytes[0]));
+ }
+
+ // The first byte should be length and there should be at least 1 more byte following to
+ // represent type.
+ // The last header byte's MSB should be 0.
+ if (!isExtending(bytes[0]) || isExtending(bytes[bytes.length - 1])) {
+ throw new IllegalArgumentException("The header format is wrong.");
+ }
+
+ return new DataElementHeader(version,
+ getTypeMultipleBytes(Arrays.copyOfRange(bytes, 1, bytes.length)),
+ getHeaderValue(bytes[0]));
+ }
+
+ /** Creates a header based on type and length.
+ * This is used when the type is <= 16 and length is <= 7. */
+ static byte createSingleByteHeader(int type, int length) {
+ return (byte) (convertTag(/* extend= */ false)
+ | convertLengthSingleByte(length)
+ | convertTypeSingleByte(type));
+ }
+
+ /** Creates a header based on type and length.
+ * This is used when the type is > 16 or length is > 7. */
+ static byte[] createMultipleBytesHeader(int type, int length) {
+ List<Byte> typeIntList = convertTypeMultipleBytes(type);
+ byte[] res = new byte[typeIntList.size() + MULTIPLE_LENGTH_BYTE];
+ int index = 0;
+ res[index++] = convertLengthMultipleBytes(length);
+
+ for (int typeInt : typeIntList) {
+ res[index++] = (byte) typeInt;
+ }
+ return res;
+ }
+
+ /** Constructs a Data Element header with length indicated in byte format.
+ * The most significant bit is the tag, 2- 4 bits are the length, 5 - 8 bits are the type.
+ */
+ @VisibleForTesting
+ static int convertLengthSingleByte(int length) {
+ Preconditions.checkArgument(length >= 0, "Length should not be negative.");
+ Preconditions.checkArgument(length < (1 << SINGLE_AVAILABLE_LENGTH_BIT),
+ "In single Data Element header, length should be shorter than 8.");
+ return (length << SINGLE_LENGTH_OFFSET) & SINGLE_LENGTH_BITMASK;
+ }
+
+ /** Constructs a Data Element header with type indicated in byte format.
+ * The most significant bit is the tag, 2- 4 bits are the length, 5 - 8 bits are the type.
+ */
+ @VisibleForTesting
+ static int convertTypeSingleByte(int type) {
+ Preconditions.checkArgument(type >= 0, "Type should not be negative.");
+ Preconditions.checkArgument(type < (1 << SINGLE_AVAILABLE_TYPE_BIT),
+ "In single Data Element header, type should be smaller than 16.");
+
+ return type & SINGLE_TYPE_BITMASK;
+ }
+
+ /**
+ * Gets the length of Data Element from the header. (When there is only 1 byte of header)
+ */
+ static int getLengthSingleByte(byte header) {
+ Preconditions.checkArgument(!isExtending(header),
+ "Cannot apply this method for the extending header.");
+ return (header & SINGLE_LENGTH_BITMASK) >> SINGLE_LENGTH_OFFSET;
+ }
+
+ /**
+ * Gets the type of Data Element from the header. (When there is only 1 byte of header)
+ */
+ static int getTypeSingleByte(byte header) {
+ Preconditions.checkArgument(!isExtending(header),
+ "Cannot apply this method for the extending header.");
+ return header & SINGLE_TYPE_BITMASK;
+ }
+
+ /** Creates a DE(data element) header based on length.
+ * This is used when header is more than 1 byte. The first byte is always the length.
+ */
+ static byte convertLengthMultipleBytes(int length) {
+ Preconditions.checkArgument(length < (1 << TAG_OFFSET),
+ "Data element should be equal or shorter than 128.");
+ return (byte) (convertTag(/* extend= */ true) | (length & MULTIPLE_BITMASK));
+ }
+
+ /** Creates a DE(data element) header based on type.
+ * This is used when header is more than 1 byte. The first byte is always the length.
+ */
+ @VisibleForTesting
+ static List<Byte> convertTypeMultipleBytes(int type) {
+ List<Byte> typeBytes = new ArrayList<>();
+ while (type > 0) {
+ byte current = (byte) (type & MULTIPLE_BITMASK);
+ type = type >> TAG_OFFSET;
+ typeBytes.add(current);
+ }
+
+ Collections.reverse(typeBytes);
+ int size = typeBytes.size();
+ // The last byte's MSB should be 0.
+ for (int i = 0; i < size - 1; i++) {
+ typeBytes.set(i, (byte) (convertTag(/* extend= */ true) | typeBytes.get(i)));
+ }
+ return typeBytes;
+ }
+
+ /** Creates a DE(data element) header based on type.
+ * This is used when header is more than 1 byte. The first byte is always the length.
+ * Uses Integer when doing bit operation to avoid error.
+ */
+ @VisibleForTesting
+ static int getTypeMultipleBytes(byte[] typeByteArray) {
+ int type = 0;
+ int size = typeByteArray.length;
+ for (int i = 0; i < size; i++) {
+ type = (type << TAG_OFFSET) | getHeaderValue(typeByteArray[i]);
+ }
+ return type;
+ }
+
+ /** Gets the integer value of the 7 bits in the header. (The MSB is tag) */
+ @VisibleForTesting
+ static int getHeaderValue(byte header) {
+ return (header & MULTIPLE_BITMASK);
+ }
+
+ /** Sets the MSB of the header byte. If this is the last byte of headers, MSB is 0.
+ * If there are at least header following, the MSB is 1.
+ */
+ @VisibleForTesting
+ static byte convertTag(boolean extend) {
+ return (byte) (extend ? 0b10000000 : 0b00000000);
+ }
+
+ /** Returns {@code true} if there are at least 1 byte of header after the current one. */
+ @VisibleForTesting
+ static boolean isExtending(byte header) {
+ return (header & TAG_BITMASK) != 0;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
new file mode 100644
index 0000000..34a7514
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceCredential;
+import android.nearby.PublicCredential;
+import android.util.Log;
+
+import com.android.server.nearby.util.encryption.Cryptor;
+import com.android.server.nearby.util.encryption.CryptorImpFake;
+import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
+import com.android.server.nearby.util.encryption.CryptorImpV1;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A Nearby Presence advertisement to be advertised on BT5.0 devices.
+ *
+ * <p>Serializable between Java object and bytes formats. Java object is used at the upper scanning
+ * and advertising interface as an abstraction of the actual bytes. Bytes format is used at the
+ * underlying BLE and mDNS stacks, which do necessary slicing and merging based on advertising
+ * capacities.
+ *
+ * The extended advertisement is defined in the format below:
+ * Header (1 byte) | salt (1+2 bytes) | Identity + filter (2+16 bytes)
+ * | repeated DE fields (various bytes)
+ * The header contains:
+ * version (3 bits) | 5 bit reserved for future use (RFU)
+ */
+public class ExtendedAdvertisement extends Advertisement{
+
+ public static final int SALT_DATA_LENGTH = 2;
+
+ static final int HEADER_LENGTH = 1;
+
+ static final int IDENTITY_DATA_LENGTH = 16;
+
+ private final List<DataElement> mDataElements;
+
+ private final byte[] mAuthenticityKey;
+
+ // All Data Elements including salt and identity.
+ // Each list item (byte array) is a Data Element (with its header).
+ private final List<byte[]> mCompleteDataElementsBytes;
+ // Signature generated from data elements.
+ private final byte[] mHmacTag;
+
+ /**
+ * Creates an {@link ExtendedAdvertisement} from a Presence Broadcast Request.
+ * @return {@link ExtendedAdvertisement} object. {@code null} when the request is illegal.
+ */
+ @Nullable
+ public static ExtendedAdvertisement createFromRequest(PresenceBroadcastRequest request) {
+ if (request.getVersion() != BroadcastRequest.PRESENCE_VERSION_V1) {
+ Log.v(TAG, "ExtendedAdvertisement only supports V1 now.");
+ return null;
+ }
+
+ byte[] salt = request.getSalt();
+ if (salt.length != SALT_DATA_LENGTH) {
+ Log.v(TAG, "Salt does not match correct length");
+ return null;
+ }
+
+ byte[] identity = request.getCredential().getMetadataEncryptionKey();
+ byte[] authenticityKey = request.getCredential().getAuthenticityKey();
+ if (identity.length != IDENTITY_DATA_LENGTH) {
+ Log.v(TAG, "Identity does not match correct length");
+ return null;
+ }
+
+ List<Integer> actions = request.getActions();
+ if (actions.isEmpty()) {
+ Log.v(TAG, "ExtendedAdvertisement must contain at least one action");
+ return null;
+ }
+
+ List<DataElement> dataElements = request.getExtendedProperties();
+ return new ExtendedAdvertisement(
+ request.getCredential().getIdentityType(),
+ identity,
+ salt,
+ authenticityKey,
+ actions,
+ dataElements);
+ }
+
+ /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
+ @Nullable
+ public byte[] toBytes() {
+ ByteBuffer buffer = ByteBuffer.allocate(getLength());
+
+ // Header
+ buffer.put(ExtendedAdvertisementUtils.constructHeader(getVersion()));
+
+ // Salt
+ buffer.put(mCompleteDataElementsBytes.get(0));
+
+ // Identity
+ buffer.put(mCompleteDataElementsBytes.get(1));
+
+ List<Byte> rawDataBytes = new ArrayList<>();
+ // Data Elements (Already includes salt and identity)
+ for (int i = 2; i < mCompleteDataElementsBytes.size(); i++) {
+ byte[] dataElementBytes = mCompleteDataElementsBytes.get(i);
+ for (Byte b : dataElementBytes) {
+ rawDataBytes.add(b);
+ }
+ }
+
+ byte[] dataElements = new byte[rawDataBytes.size()];
+ for (int i = 0; i < rawDataBytes.size(); i++) {
+ dataElements[i] = rawDataBytes.get(i);
+ }
+
+ buffer.put(
+ getCryptor(/* encrypt= */ true).encrypt(dataElements, getSalt(), mAuthenticityKey));
+
+ buffer.put(mHmacTag);
+
+ return buffer.array();
+ }
+
+ /** Deserialize from bytes into an {@link ExtendedAdvertisement} object.
+ * {@code null} when there is something when parsing.
+ */
+ @Nullable
+ public static ExtendedAdvertisement fromBytes(byte[] bytes, PublicCredential publicCredential) {
+ @BroadcastRequest.BroadcastVersion
+ int version = ExtendedAdvertisementUtils.getVersion(bytes);
+ if (version != PresenceBroadcastRequest.PRESENCE_VERSION_V1) {
+ Log.v(TAG, "ExtendedAdvertisement is used in V1 only and version is " + version);
+ return null;
+ }
+
+ byte[] authenticityKey = publicCredential.getAuthenticityKey();
+
+ int index = HEADER_LENGTH;
+ // Salt
+ byte[] saltHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
+ DataElementHeader saltHeader = DataElementHeader.fromBytes(version, saltHeaderArray);
+ if (saltHeader == null || saltHeader.getDataType() != DataElement.DataType.SALT) {
+ Log.v(TAG, "First data element has to be salt.");
+ return null;
+ }
+ index += saltHeaderArray.length;
+ byte[] salt = new byte[saltHeader.getDataLength()];
+ for (int i = 0; i < saltHeader.getDataLength(); i++) {
+ salt[i] = bytes[index++];
+ }
+
+ // Identity
+ byte[] identityHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
+ DataElementHeader identityHeader =
+ DataElementHeader.fromBytes(version, identityHeaderArray);
+ if (identityHeader == null) {
+ Log.v(TAG, "The second element has to be identity.");
+ return null;
+ }
+ index += identityHeaderArray.length;
+ @PresenceCredential.IdentityType int identityType =
+ toPresenceCredentialIdentityType(identityHeader.getDataType());
+ if (identityType == PresenceCredential.IDENTITY_TYPE_UNKNOWN) {
+ Log.v(TAG, "The identity type is unknown.");
+ return null;
+ }
+ byte[] encryptedIdentity = new byte[identityHeader.getDataLength()];
+ for (int i = 0; i < identityHeader.getDataLength(); i++) {
+ encryptedIdentity[i] = bytes[index++];
+ }
+ byte[] identity =
+ CryptorImpIdentityV1
+ .getInstance().decrypt(encryptedIdentity, salt, authenticityKey);
+
+ Cryptor cryptor = getCryptor(/* encrypt= */ true);
+ byte[] encryptedDataElements =
+ new byte[bytes.length - index - cryptor.getSignatureLength()];
+ // Decrypt other data elements
+ System.arraycopy(bytes, index, encryptedDataElements, 0, encryptedDataElements.length);
+ byte[] decryptedDataElements =
+ cryptor.decrypt(encryptedDataElements, salt, authenticityKey);
+ if (decryptedDataElements == null) {
+ return null;
+ }
+
+ // Verify the computed HMAC tag is equal to HMAC tag in advertisement
+ if (cryptor.getSignatureLength() > 0) {
+ byte[] expectedHmacTag = new byte[cryptor.getSignatureLength()];
+ System.arraycopy(
+ bytes, bytes.length - cryptor.getSignatureLength(),
+ expectedHmacTag, 0, cryptor.getSignatureLength());
+ if (!cryptor.verify(decryptedDataElements, authenticityKey, expectedHmacTag)) {
+ Log.e(TAG, "HMAC tags not match.");
+ return null;
+ }
+ }
+
+ int dataElementArrayIndex = 0;
+ // Other Data Elements
+ List<Integer> actions = new ArrayList<>();
+ List<DataElement> dataElements = new ArrayList<>();
+ while (dataElementArrayIndex < decryptedDataElements.length) {
+ byte[] deHeaderArray = ExtendedAdvertisementUtils
+ .getDataElementHeader(decryptedDataElements, dataElementArrayIndex);
+ DataElementHeader deHeader = DataElementHeader.fromBytes(version, deHeaderArray);
+ dataElementArrayIndex += deHeaderArray.length;
+
+ @DataElement.DataType int type = Objects.requireNonNull(deHeader).getDataType();
+ if (type == DataElement.DataType.ACTION) {
+ if (deHeader.getDataLength() != 1) {
+ Log.v(TAG, "Action id should only 1 byte.");
+ return null;
+ }
+ actions.add((int) decryptedDataElements[dataElementArrayIndex++]);
+ } else {
+ if (isSaltOrIdentity(type)) {
+ Log.v(TAG, "Type " + type + " is duplicated. There should be only one salt"
+ + " and one identity in the advertisement.");
+ return null;
+ }
+ byte[] deData = new byte[deHeader.getDataLength()];
+ for (int i = 0; i < deHeader.getDataLength(); i++) {
+ deData[i] = decryptedDataElements[dataElementArrayIndex++];
+ }
+ dataElements.add(new DataElement(type, deData));
+ }
+ }
+
+ return new ExtendedAdvertisement(identityType, identity, salt, authenticityKey, actions,
+ dataElements);
+ }
+
+ /** Returns the {@link DataElement}s in the advertisement. */
+ public List<DataElement> getDataElements() {
+ return new ArrayList<>(mDataElements);
+ }
+
+ /** Returns the {@link DataElement}s in the advertisement according to the key. */
+ public List<DataElement> getDataElements(@DataElement.DataType int key) {
+ List<DataElement> res = new ArrayList<>();
+ for (DataElement dataElement : mDataElements) {
+ if (key == dataElement.getKey()) {
+ res.add(dataElement);
+ }
+ }
+ return res;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "ExtendedAdvertisement:"
+ + "<VERSION: %s, length: %s, dataElementCount: %s, identityType: %s,"
+ + " identity: %s, salt: %s, actions: %s>",
+ getVersion(),
+ getLength(),
+ getDataElements().size(),
+ getIdentityType(),
+ Arrays.toString(getIdentity()),
+ Arrays.toString(getSalt()),
+ getActions());
+ }
+
+ ExtendedAdvertisement(
+ @PresenceCredential.IdentityType int identityType,
+ byte[] identity,
+ byte[] salt,
+ byte[] authenticityKey,
+ List<Integer> actions,
+ List<DataElement> dataElements) {
+ this.mVersion = BroadcastRequest.PRESENCE_VERSION_V1;
+ this.mIdentityType = identityType;
+ this.mIdentity = identity;
+ this.mSalt = salt;
+ this.mAuthenticityKey = authenticityKey;
+ this.mActions = actions;
+ this.mDataElements = dataElements;
+ this.mCompleteDataElementsBytes = new ArrayList<>();
+
+ int length = HEADER_LENGTH; // header
+
+ // Salt
+ DataElement saltElement = new DataElement(DataElement.DataType.SALT, salt);
+ byte[] saltByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(saltElement);
+ mCompleteDataElementsBytes.add(saltByteArray);
+ length += saltByteArray.length;
+
+ // Identity
+ byte[] encryptedIdentity =
+ CryptorImpIdentityV1.getInstance().encrypt(identity, salt, authenticityKey);
+ DataElement identityElement = new DataElement(toDataType(identityType), encryptedIdentity);
+ byte[] identityByteArray =
+ ExtendedAdvertisementUtils.convertDataElementToBytes(identityElement);
+ mCompleteDataElementsBytes.add(identityByteArray);
+ length += identityByteArray.length;
+
+ List<Byte> dataElementBytes = new ArrayList<>();
+ // Intents
+ for (int action : mActions) {
+ DataElement actionElement = new DataElement(DataElement.DataType.ACTION,
+ new byte[] {(byte) action});
+ byte[] intentByteArray =
+ ExtendedAdvertisementUtils.convertDataElementToBytes(actionElement);
+ mCompleteDataElementsBytes.add(intentByteArray);
+ for (Byte b : intentByteArray) {
+ dataElementBytes.add(b);
+ }
+ }
+
+ // Data Elements (Extended properties)
+ for (DataElement dataElement : mDataElements) {
+ byte[] deByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(dataElement);
+ mCompleteDataElementsBytes.add(deByteArray);
+ for (Byte b : deByteArray) {
+ dataElementBytes.add(b);
+ }
+ }
+
+ byte[] data = new byte[dataElementBytes.size()];
+ for (int i = 0; i < dataElementBytes.size(); i++) {
+ data[i] = dataElementBytes.get(i);
+ }
+ Cryptor cryptor = getCryptor(/* encrypt= */ true);
+ byte[] encryptedDeBytes = cryptor.encrypt(data, salt, authenticityKey);
+
+ length += encryptedDeBytes.length;
+
+ // Signature
+ byte[] hmacTag = Objects.requireNonNull(cryptor.sign(data, authenticityKey));
+ mHmacTag = hmacTag;
+ length += hmacTag.length;
+
+ this.mLength = length;
+ }
+
+ @PresenceCredential.IdentityType
+ private static int toPresenceCredentialIdentityType(@DataElement.DataType int type) {
+ switch (type) {
+ case DataElement.DataType.PRIVATE_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_PRIVATE;
+ case DataElement.DataType.PROVISIONED_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_PROVISIONED;
+ case DataElement.DataType.TRUSTED_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_TRUSTED;
+ case DataElement.DataType.PUBLIC_IDENTITY:
+ default:
+ return PresenceCredential.IDENTITY_TYPE_UNKNOWN;
+ }
+ }
+
+ @DataElement.DataType
+ private static int toDataType(@PresenceCredential.IdentityType int identityType) {
+ switch (identityType) {
+ case PresenceCredential.IDENTITY_TYPE_PRIVATE:
+ return DataElement.DataType.PRIVATE_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_PROVISIONED:
+ return DataElement.DataType.PROVISIONED_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_TRUSTED:
+ return DataElement.DataType.TRUSTED_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_UNKNOWN:
+ default:
+ return DataElement.DataType.PUBLIC_IDENTITY;
+ }
+ }
+
+ /**
+ * Returns {@code true} if the given {@link DataElement.DataType} is salt, or one of the
+ * identities. Identities should be able to convert to {@link PresenceCredential.IdentityType}s.
+ */
+ private static boolean isSaltOrIdentity(@DataElement.DataType int type) {
+ return type == DataElement.DataType.SALT || type == DataElement.DataType.PRIVATE_IDENTITY
+ || type == DataElement.DataType.TRUSTED_IDENTITY
+ || type == DataElement.DataType.PROVISIONED_IDENTITY
+ || type == DataElement.DataType.PUBLIC_IDENTITY;
+ }
+
+ private static Cryptor getCryptor(boolean encrypt) {
+ if (encrypt) {
+ Log.d(TAG, "get V1 Cryptor");
+ return CryptorImpV1.getInstance();
+ }
+ Log.d(TAG, "get fake Cryptor");
+ return CryptorImpFake.getInstance();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisementUtils.java b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisementUtils.java
new file mode 100644
index 0000000..06d0f2b
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisementUtils.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.android.server.nearby.presence.ExtendedAdvertisement.HEADER_LENGTH;
+
+import android.annotation.SuppressLint;
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides serialization and deserialization util methods for {@link ExtendedAdvertisement}.
+ */
+public final class ExtendedAdvertisementUtils {
+
+ // Advertisement header related static fields.
+ private static final int VERSION_MASK = 0b11100000;
+ private static final int VERSION_MASK_AFTER_SHIT = 0b00000111;
+ private static final int HEADER_INDEX = 0;
+ private static final int HEADER_VERSION_OFFSET = 5;
+
+ /**
+ * Constructs the header of a {@link ExtendedAdvertisement}.
+ * 3 bit version, and 5 bit reserved for future use (RFU).
+ */
+ public static byte constructHeader(@BroadcastRequest.BroadcastVersion int version) {
+ return (byte) ((version << 5) & VERSION_MASK);
+ }
+
+ /** Returns the {@link BroadcastRequest.BroadcastVersion} from the advertisement
+ * in bytes format. */
+ public static int getVersion(byte[] advertisement) {
+ if (advertisement.length < HEADER_LENGTH) {
+ throw new IllegalArgumentException("Advertisement must contain header");
+ }
+ return ((advertisement[HEADER_INDEX] & VERSION_MASK) >> HEADER_VERSION_OFFSET)
+ & VERSION_MASK_AFTER_SHIT;
+ }
+
+ /** Returns the {@link DataElementHeader} from the advertisement in bytes format. */
+ public static byte[] getDataElementHeader(byte[] advertisement, int startIndex) {
+ Preconditions.checkArgument(startIndex < advertisement.length,
+ "Advertisement has no longer data left.");
+ List<Byte> headerBytes = new ArrayList<>();
+ while (startIndex < advertisement.length) {
+ byte current = advertisement[startIndex];
+ headerBytes.add(current);
+ if (!DataElementHeader.isExtending(current)) {
+ int size = headerBytes.size();
+ byte[] res = new byte[size];
+ for (int i = 0; i < size; i++) {
+ res[i] = headerBytes.get(i);
+ }
+ return res;
+ }
+ startIndex++;
+ }
+ throw new IllegalArgumentException("There is no end of the DataElement header.");
+ }
+
+ /**
+ * Constructs {@link DataElement}, including header(s) and actual data element data.
+ *
+ * Suppresses warning because {@link DataElement} checks isValidType in constructor.
+ */
+ @SuppressLint("WrongConstant")
+ public static byte[] convertDataElementToBytes(DataElement dataElement) {
+ @DataElement.DataType int type = dataElement.getKey();
+ byte[] data = dataElement.getValue();
+ DataElementHeader header = new DataElementHeader(BroadcastRequest.PRESENCE_VERSION_V1,
+ type, data.length);
+ byte[] headerByteArray = header.toBytes();
+
+ byte[] res = new byte[headerByteArray.length + data.length];
+ System.arraycopy(headerByteArray, 0, res, 0, headerByteArray.length);
+ System.arraycopy(data, 0, res, headerByteArray.length, data.length);
+ return res;
+ }
+
+ private ExtendedAdvertisementUtils() {}
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java
index e4df673..ae53ada 100644
--- a/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java
+++ b/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java
@@ -24,7 +24,6 @@
import com.android.internal.util.Preconditions;
import java.nio.ByteBuffer;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -42,7 +41,7 @@
// The header contains:
// version (3 bits) | provision_mode_flag (1 bit) | identity_type (3 bits) |
// extended_advertisement_mode (1 bit)
-public class FastAdvertisement {
+public class FastAdvertisement extends Advertisement {
private static final int FAST_ADVERTISEMENT_MAX_LENGTH = 24;
@@ -85,7 +84,8 @@
(byte) request.getTxPower());
}
- /** Serialize an {@link FastAdvertisement} object into bytes. */
+ /** Serialize a {@link FastAdvertisement} object into bytes. */
+ @Override
public byte[] toBytes() {
ByteBuffer buffer = ByteBuffer.allocate(getLength());
@@ -100,18 +100,8 @@
return buffer.array();
}
- private final int mLength;
-
private final int mLtvFieldCount;
- @PresenceCredential.IdentityType private final int mIdentityType;
-
- private final byte[] mIdentity;
-
- private final byte[] mSalt;
-
- private final List<Integer> mActions;
-
@Nullable
private final Byte mTxPower;
@@ -121,6 +111,7 @@
byte[] salt,
List<Integer> actions,
@Nullable Byte txPower) {
+ this.mVersion = BroadcastRequest.PRESENCE_VERSION_V0;
this.mIdentityType = identityType;
this.mIdentity = identity;
this.mSalt = salt;
@@ -143,44 +134,12 @@
"FastAdvertisement exceeds maximum length");
}
- /** Returns the version in the advertisement. */
- @BroadcastRequest.BroadcastVersion
- public int getVersion() {
- return BroadcastRequest.PRESENCE_VERSION_V0;
- }
-
- /** Returns the identity type in the advertisement. */
- @PresenceCredential.IdentityType
- public int getIdentityType() {
- return mIdentityType;
- }
-
- /** Returns the identity bytes in the advertisement. */
- public byte[] getIdentity() {
- return mIdentity.clone();
- }
-
- /** Returns the salt of the advertisement. */
- public byte[] getSalt() {
- return mSalt.clone();
- }
-
- /** Returns the actions in the advertisement. */
- public List<Integer> getActions() {
- return new ArrayList<>(mActions);
- }
-
/** Returns the adjusted TX Power in the advertisement. Null if not available. */
@Nullable
public Byte getTxPower() {
return mTxPower;
}
- /** Returns the length of the advertisement. */
- public int getLength() {
- return mLength;
- }
-
/** Returns the count of LTV fields in the advertisement. */
public int getLtvFieldCount() {
return mLtvFieldCount;
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java b/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
index d1c72ae..5a76d96 100644
--- a/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
@@ -16,31 +16,55 @@
package com.android.server.nearby.presence;
-import android.nearby.NearbyDevice;
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.NonNull;
+import android.nearby.DataElement;
import android.nearby.NearbyDeviceParcelable;
import android.nearby.PresenceDevice;
import android.nearby.PresenceScanFilter;
import android.nearby.PublicCredential;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Set;
/** Represents a Presence discovery result. */
public class PresenceDiscoveryResult {
/** Creates a {@link PresenceDiscoveryResult} from the scan data. */
public static PresenceDiscoveryResult fromDevice(NearbyDeviceParcelable device) {
+ PresenceDevice presenceDevice = device.getPresenceDevice();
+ if (presenceDevice != null) {
+ return new PresenceDiscoveryResult.Builder()
+ .setTxPower(device.getTxPower())
+ .setRssi(device.getRssi())
+ .setSalt(presenceDevice.getSalt())
+ .setPublicCredential(device.getPublicCredential())
+ .addExtendedProperties(presenceDevice.getExtendedProperties())
+ .setEncryptedIdentityTag(device.getEncryptionKeyTag())
+ .build();
+ }
byte[] salt = device.getSalt();
if (salt == null) {
salt = new byte[0];
}
- return new PresenceDiscoveryResult.Builder()
- .setTxPower(device.getTxPower())
+
+ PresenceDiscoveryResult.Builder builder = new PresenceDiscoveryResult.Builder();
+ builder.setTxPower(device.getTxPower())
.setRssi(device.getRssi())
.setSalt(salt)
.addPresenceAction(device.getAction())
- .setPublicCredential(device.getPublicCredential())
- .build();
+ .setPublicCredential(device.getPublicCredential());
+ if (device.getPresenceDevice() != null) {
+ builder.addExtendedProperties(device.getPresenceDevice().getExtendedProperties());
+ }
+ return builder.build();
}
private final int mTxPower;
@@ -48,25 +72,35 @@
private final byte[] mSalt;
private final List<Integer> mPresenceActions;
private final PublicCredential mPublicCredential;
+ private final List<DataElement> mExtendedProperties;
+ private final byte[] mEncryptedIdentityTag;
private PresenceDiscoveryResult(
int txPower,
int rssi,
byte[] salt,
List<Integer> presenceActions,
- PublicCredential publicCredential) {
+ PublicCredential publicCredential,
+ List<DataElement> extendedProperties,
+ byte[] encryptedIdentityTag) {
mTxPower = txPower;
mRssi = rssi;
mSalt = salt;
mPresenceActions = presenceActions;
mPublicCredential = publicCredential;
+ mExtendedProperties = extendedProperties;
+ mEncryptedIdentityTag = encryptedIdentityTag;
}
/** Returns whether the discovery result matches the scan filter. */
public boolean matches(PresenceScanFilter scanFilter) {
+ if (accountKeyMatches(scanFilter.getExtendedProperties())) {
+ return true;
+ }
+
return pathLossMatches(scanFilter.getMaxPathLoss())
&& actionMatches(scanFilter.getPresenceActions())
- && credentialMatches(scanFilter.getCredentials());
+ && identityMatches(scanFilter.getCredentials());
}
private boolean pathLossMatches(int maxPathLoss) {
@@ -80,21 +114,47 @@
return filterActions.stream().anyMatch(mPresenceActions::contains);
}
- private boolean credentialMatches(List<PublicCredential> credentials) {
- return credentials.contains(mPublicCredential);
+ @VisibleForTesting
+ boolean accountKeyMatches(List<DataElement> extendedProperties) {
+ Set<byte[]> accountKeys = new ArraySet<>();
+ for (DataElement requestedDe : mExtendedProperties) {
+ if (requestedDe.getKey() != DataElement.DataType.ACCOUNT_KEY_DATA) {
+ continue;
+ }
+ accountKeys.add(requestedDe.getValue());
+ }
+ for (DataElement scannedDe : extendedProperties) {
+ if (scannedDe.getKey() != DataElement.DataType.ACCOUNT_KEY_DATA) {
+ continue;
+ }
+ // If one account key matches, then returns true.
+ for (byte[] key : accountKeys) {
+ if (Arrays.equals(key, scannedDe.getValue())) {
+ return true;
+ }
+ }
+ }
+
+ return false;
}
- /** Converts a presence device from the discovery result. */
- public PresenceDevice toPresenceDevice() {
- return new PresenceDevice.Builder(
- // Use the public credential hash as the device Id.
- String.valueOf(mPublicCredential.hashCode()),
- mSalt,
- mPublicCredential.getSecretId(),
- mPublicCredential.getEncryptedMetadata())
- .setRssi(mRssi)
- .addMedium(NearbyDevice.Medium.BLE)
- .build();
+ @VisibleForTesting
+ /** Gets presence {@link DataElement}s of the discovery result. */
+ public List<DataElement> getExtendedProperties() {
+ return mExtendedProperties;
+ }
+
+ private boolean identityMatches(List<PublicCredential> publicCredentials) {
+ if (mEncryptedIdentityTag.length == 0) {
+ return true;
+ }
+ for (PublicCredential publicCredential : publicCredentials) {
+ if (Arrays.equals(
+ mEncryptedIdentityTag, publicCredential.getEncryptedMetadataKeyTag())) {
+ return true;
+ }
+ }
+ return false;
}
/** Builder for {@link PresenceDiscoveryResult}. */
@@ -105,9 +165,12 @@
private PublicCredential mPublicCredential;
private final List<Integer> mPresenceActions;
+ private final List<DataElement> mExtendedProperties;
+ private byte[] mEncryptedIdentityTag = new byte[0];
public Builder() {
mPresenceActions = new ArrayList<>();
+ mExtendedProperties = new ArrayList<>();
}
/** Sets the calibrated tx power for the discovery result. */
@@ -130,7 +193,18 @@
/** Sets the public credential for the discovery result. */
public Builder setPublicCredential(PublicCredential publicCredential) {
- mPublicCredential = publicCredential;
+ if (publicCredential != null) {
+ mPublicCredential = publicCredential;
+ }
+ return this;
+ }
+
+ /** Sets the encrypted identity tag for the discovery result. Usually it is passed from
+ * {@link NearbyDeviceParcelable} and the tag is calculated with authenticity key when
+ * receiving an advertisement.
+ */
+ public Builder setEncryptedIdentityTag(byte[] encryptedIdentityTag) {
+ mEncryptedIdentityTag = encryptedIdentityTag;
return this;
}
@@ -140,10 +214,34 @@
return this;
}
+ /** Adds presence {@link DataElement}s of the discovery result. */
+ public Builder addExtendedProperties(DataElement dataElement) {
+ if (dataElement.getKey() == DataElement.DataType.ACTION) {
+ byte[] value = dataElement.getValue();
+ if (value.length == 1) {
+ addPresenceAction(Byte.toUnsignedInt(value[0]));
+ } else {
+ Log.e(TAG, "invalid action data element");
+ }
+ } else {
+ mExtendedProperties.add(dataElement);
+ }
+ return this;
+ }
+
+ /** Adds presence {@link DataElement}s of the discovery result. */
+ public Builder addExtendedProperties(@NonNull List<DataElement> dataElements) {
+ for (DataElement dataElement : dataElements) {
+ addExtendedProperties(dataElement);
+ }
+ return this;
+ }
+
/** Builds a {@link PresenceDiscoveryResult}. */
public PresenceDiscoveryResult build() {
return new PresenceDiscoveryResult(
- mTxPower, mRssi, mSalt, mPresenceActions, mPublicCredential);
+ mTxPower, mRssi, mSalt, mPresenceActions,
+ mPublicCredential, mExtendedProperties, mEncryptedIdentityTag);
}
}
}
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
new file mode 100644
index 0000000..0a51068
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nearby.DataElement;
+import android.nearby.NearbyDevice;
+import android.nearby.NearbyManager;
+import android.nearby.PresenceDevice;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanCallback;
+import android.nearby.ScanRequest;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Executors;
+
+/** PresenceManager is the class initiated in nearby service to handle presence related work. */
+public class PresenceManager {
+
+ final Context mContext;
+ private final IntentFilter mIntentFilter;
+
+ @VisibleForTesting
+ final ScanCallback mScanCallback =
+ new ScanCallback() {
+ @Override
+ public void onDiscovered(@NonNull NearbyDevice device) {
+ Log.i(TAG, "[PresenceManager] discovered Device.");
+ PresenceDevice presenceDevice = (PresenceDevice) device;
+ List<DataElement> dataElements = presenceDevice.getExtendedProperties();
+ for (DataElement dataElement : dataElements) {
+ Log.i(TAG, "[PresenceManager] Data Element key "
+ + dataElement.getKey());
+ Log.i(TAG, "[PresenceManager] Data Element value "
+ + Arrays.toString(dataElement.getValue()));
+ }
+ }
+
+ @Override
+ public void onUpdated(@NonNull NearbyDevice device) {}
+
+ @Override
+ public void onLost(@NonNull NearbyDevice device) {}
+
+ @Override
+ public void onError(int errorCode) {
+ Log.w(TAG, "[PresenceManager] Scan error is " + errorCode);
+ }
+ };
+
+ private final BroadcastReceiver mScreenBroadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ NearbyManager manager = getNearbyManager();
+ if (manager == null) {
+ Log.e(TAG, "Nearby Manager is null");
+ return;
+ }
+ if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
+ Log.d(TAG, "PresenceManager Start scan.");
+ PublicCredential publicCredential =
+ new PublicCredential.Builder(new byte[]{1}, new byte[]{1},
+ new byte[]{1}, new byte[]{1}, new byte[]{1}).build();
+ PresenceScanFilter presenceScanFilter =
+ new PresenceScanFilter.Builder()
+ .setMaxPathLoss(3)
+ .addCredential(publicCredential)
+ .addPresenceAction(1)
+ .addExtendedProperty(new DataElement(
+ DataElement.DataType.ACCOUNT_KEY_DATA,
+ new byte[16]))
+ .build();
+ ScanRequest scanRequest =
+ new ScanRequest.Builder()
+ .setScanType(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(presenceScanFilter)
+ .build();
+ Log.d(
+ TAG,
+ String.format(
+ Locale.getDefault(),
+ "[PresenceManager] Start Presence scan with request: %s",
+ scanRequest.toString()));
+ manager.startScan(
+ scanRequest, Executors.newSingleThreadExecutor(), mScanCallback);
+ } else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
+ Log.d(TAG, "PresenceManager Stop scan.");
+ manager.stopScan(mScanCallback);
+ }
+ }
+ };
+
+ public PresenceManager(Context context) {
+ mContext = context;
+ mIntentFilter = new IntentFilter();
+ }
+
+ /** Null when the Nearby Service is not available. */
+ @Nullable
+ private NearbyManager getNearbyManager() {
+ return (NearbyManager)
+ mContext.getApplicationContext()
+ .getSystemService(Context.NEARBY_SERVICE);
+ }
+
+ /** Function called when nearby service start. */
+ public void initiate() {
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ mContext.registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
index f136695..3de6ff0 100644
--- a/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.nearby.NearbyDeviceParcelable;
+import android.nearby.ScanCallback;
import android.nearby.ScanFilter;
import android.nearby.ScanRequest;
import android.util.Log;
@@ -40,15 +41,6 @@
protected final DiscoveryProviderController mController;
protected final Executor mExecutor;
protected Listener mListener;
- protected List<ScanFilter> mScanFilters;
-
- /** Interface for listening to discovery providers. */
- public interface Listener {
- /**
- * Called when a provider has a new nearby device available. May be invoked from any thread.
- */
- void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice);
- }
protected AbstractDiscoveryProvider(Context context, Executor executor) {
mContext = context;
@@ -77,14 +69,33 @@
protected void invalidateScanMode() {}
/**
+ * Callback invoked to inform the provider of new provider scan filters which replaces any prior
+ * provider filters. Always invoked on the provider executor.
+ */
+ protected void onSetScanFilters(List<ScanFilter> filters) {}
+
+ /**
* Retrieves the controller for this discovery provider. Should never be invoked by subclasses,
* as a discovery provider should not be controlling itself. Using this method from subclasses
* could also result in deadlock.
*/
- protected DiscoveryProviderController getController() {
+ public DiscoveryProviderController getController() {
return mController;
}
+ /** Interface for listening to discovery providers. */
+ public interface Listener {
+ /**
+ * Called when a provider has a new nearby device available. May be invoked from any thread.
+ */
+ void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice);
+
+ /**
+ * Called when a provider found error from the scan.
+ */
+ void onError(@ScanCallback.ErrorCode int errorCode);
+ }
+
private class Controller implements DiscoveryProviderController {
private boolean mStarted = false;
@@ -120,6 +131,12 @@
mExecutor.execute(AbstractDiscoveryProvider.this::onStop);
}
+ @ScanRequest.ScanMode
+ @Override
+ public int getProviderScanMode() {
+ return mScanMode;
+ }
+
@Override
public void setProviderScanMode(@ScanRequest.ScanMode int scanMode) {
if (mScanMode == scanMode) {
@@ -130,15 +147,9 @@
mExecutor.execute(AbstractDiscoveryProvider.this::invalidateScanMode);
}
- @ScanRequest.ScanMode
- @Override
- public int getProviderScanMode() {
- return mScanMode;
- }
-
@Override
public void setProviderScanFilters(List<ScanFilter> filters) {
- mScanFilters = filters;
+ mExecutor.execute(() -> onSetScanFilters(filters));
}
}
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 67392ad..6829fba 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -16,17 +16,24 @@
package com.android.server.nearby.provider;
+import static com.android.server.nearby.NearbyService.TAG;
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID;
+
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.AdvertisingSet;
+import android.bluetooth.le.AdvertisingSetCallback;
+import android.bluetooth.le.AdvertisingSetParameters;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.nearby.BroadcastCallback;
-import android.os.ParcelUuid;
+import android.nearby.BroadcastRequest;
+import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.injector.Injector;
-import java.util.UUID;
import java.util.concurrent.Executor;
/**
@@ -37,7 +44,7 @@
/**
* Listener for Broadcast status changes.
*/
- interface BroadcastListener {
+ public interface BroadcastListener {
void onStatusChanged(int status);
}
@@ -46,13 +53,19 @@
private BroadcastListener mBroadcastListener;
private boolean mIsAdvertising;
-
- BleBroadcastProvider(Injector injector, Executor executor) {
+ @VisibleForTesting
+ AdvertisingSetCallback mAdvertisingSetCallback;
+ public BleBroadcastProvider(Injector injector, Executor executor) {
mInjector = injector;
mExecutor = executor;
+ mAdvertisingSetCallback = getAdvertisingSetCallback();
}
- void start(byte[] advertisementPackets, BroadcastListener listener) {
+ /**
+ * Starts to broadcast with given bytes.
+ */
+ public void start(@BroadcastRequest.BroadcastVersion int version, byte[] advertisementPackets,
+ BroadcastListener listener) {
if (mIsAdvertising) {
stop();
}
@@ -63,23 +76,35 @@
mInjector.getBluetoothAdapter().getBluetoothLeAdvertiser();
if (bluetoothLeAdvertiser != null) {
advertiseStarted = true;
- AdvertiseSettings settings =
- new AdvertiseSettings.Builder()
- .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
- .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
- .setConnectable(true)
- .build();
-
- // TODO(b/230538655) Use empty data until Presence V1 protocol is implemented.
- ParcelUuid emptyParcelUuid = new ParcelUuid(new UUID(0L, 0L));
- byte[] emptyAdvertisementPackets = new byte[0];
AdvertiseData advertiseData =
new AdvertiseData.Builder()
- .addServiceData(emptyParcelUuid, emptyAdvertisementPackets).build();
+ .addServiceData(PRESENCE_UUID, advertisementPackets).build();
try {
mBroadcastListener = listener;
- bluetoothLeAdvertiser.startAdvertising(settings, advertiseData, this);
+ switch (version) {
+ case BroadcastRequest.PRESENCE_VERSION_V0:
+ bluetoothLeAdvertiser.startAdvertising(getAdvertiseSettings(),
+ advertiseData, this);
+ break;
+ case BroadcastRequest.PRESENCE_VERSION_V1:
+ if (adapter.isLeExtendedAdvertisingSupported()) {
+ bluetoothLeAdvertiser.startAdvertisingSet(
+ getAdvertisingSetParameters(),
+ advertiseData,
+ null, null, null, mAdvertisingSetCallback);
+ } else {
+ Log.w(TAG, "Failed to start advertising set because the chipset"
+ + " does not supports LE Extended Advertising feature.");
+ advertiseStarted = false;
+ }
+ break;
+ default:
+ Log.w(TAG, "Failed to start advertising set because the advertisement"
+ + " is wrong.");
+ advertiseStarted = false;
+ }
} catch (NullPointerException | IllegalStateException | SecurityException e) {
+ Log.w(TAG, "Failed to start advertising.", e);
advertiseStarted = false;
}
}
@@ -89,7 +114,10 @@
}
}
- void stop() {
+ /**
+ * Stops current advertisement.
+ */
+ public void stop() {
if (mIsAdvertising) {
BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
if (adapter != null) {
@@ -97,6 +125,7 @@
mInjector.getBluetoothAdapter().getBluetoothLeAdvertiser();
if (bluetoothLeAdvertiser != null) {
bluetoothLeAdvertiser.stopAdvertising(this);
+ bluetoothLeAdvertiser.stopAdvertisingSet(mAdvertisingSetCallback);
}
}
mBroadcastListener = null;
@@ -120,4 +149,41 @@
mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_FAILURE);
}
}
+
+ private static AdvertiseSettings getAdvertiseSettings() {
+ return new AdvertiseSettings.Builder()
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
+ .setConnectable(true)
+ .build();
+ }
+
+ private static AdvertisingSetParameters getAdvertisingSetParameters() {
+ return new AdvertisingSetParameters.Builder()
+ .setInterval(AdvertisingSetParameters.INTERVAL_MEDIUM)
+ .setTxPowerLevel(AdvertisingSetParameters.TX_POWER_MEDIUM)
+ .setIncludeTxPower(true)
+ .setConnectable(true)
+ .build();
+ }
+
+ private AdvertisingSetCallback getAdvertisingSetCallback() {
+ return new AdvertisingSetCallback() {
+ @Override
+ public void onAdvertisingSetStarted(AdvertisingSet advertisingSet,
+ int txPower, int status) {
+ if (status == AdvertisingSetCallback.ADVERTISE_SUCCESS) {
+ if (mBroadcastListener != null) {
+ mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_OK);
+ }
+ mIsAdvertising = true;
+ } else {
+ Log.e(TAG, "Starts advertising failed in status " + status);
+ if (mBroadcastListener != null) {
+ mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_FAILURE);
+ }
+ }
+ }
+ };
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
index e2fbe77..355f7cf 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -16,6 +16,8 @@
package com.android.server.nearby.provider;
+import static android.nearby.ScanCallback.ERROR_UNKNOWN;
+
import static com.android.server.nearby.NearbyService.TAG;
import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID;
@@ -28,18 +30,27 @@
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
+import android.nearby.DataElement;
import android.nearby.NearbyDevice;
import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceDevice;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
import android.nearby.ScanRequest;
import android.os.ParcelUuid;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.ExtendedAdvertisement;
+import com.android.server.nearby.util.ArrayUtils;
import com.android.server.nearby.util.ForegroundThread;
+import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
@@ -52,15 +63,32 @@
// Don't block the thread as it may be used by other services.
private static final Executor NEARBY_EXECUTOR = ForegroundThread.getExecutor();
private final Injector mInjector;
+ private final Object mLock = new Object();
+ // Null when the filters are never set
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ @Nullable
+ private List<android.nearby.ScanFilter> mScanFilters;
+ private android.bluetooth.le.ScanCallback mScanCallbackLegacy =
+ new android.bluetooth.le.ScanCallback() {
+ @Override
+ public void onScanResult(int callbackType, ScanResult scanResult) {
+ }
+ @Override
+ public void onScanFailed(int errorCode) {
+ }
+ };
private android.bluetooth.le.ScanCallback mScanCallback =
new android.bluetooth.le.ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult scanResult) {
NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder();
- builder.setMedium(NearbyDevice.Medium.BLE)
+ String bleAddress = scanResult.getDevice().getAddress();
+ builder.setDeviceId(bleAddress.hashCode())
+ .setMedium(NearbyDevice.Medium.BLE)
.setRssi(scanResult.getRssi())
.setTxPower(scanResult.getTxPower())
- .setBluetoothAddress(scanResult.getDevice().getAddress());
+ .setBluetoothAddress(bleAddress);
ScanRecord record = scanResult.getScanRecord();
if (record != null) {
@@ -72,7 +100,8 @@
if (serviceDataMap != null) {
byte[] presenceData = serviceDataMap.get(PRESENCE_UUID);
if (presenceData != null) {
- builder.setData(serviceDataMap.get(PRESENCE_UUID));
+ setPresenceDevice(presenceData, builder, deviceName,
+ scanResult.getRssi());
}
}
}
@@ -81,7 +110,8 @@
@Override
public void onScanFailed(int errorCode) {
- Log.w(TAG, "BLE Scan failed with error code " + errorCode);
+ Log.w(TAG, "BLE 5.0 Scan failed with error code " + errorCode);
+ mExecutor.execute(() -> mListener.onError(ERROR_UNKNOWN));
}
};
@@ -90,6 +120,29 @@
mInjector = injector;
}
+ private static PresenceDevice getPresenceDevice(ExtendedAdvertisement advertisement,
+ String deviceName, int rssi) {
+ // TODO(238458326): After implementing encryption, use real data.
+ byte[] secretIdBytes = new byte[0];
+ PresenceDevice.Builder builder =
+ new PresenceDevice.Builder(
+ String.valueOf(advertisement.hashCode()),
+ advertisement.getSalt(),
+ secretIdBytes,
+ advertisement.getIdentity())
+ .addMedium(NearbyDevice.Medium.BLE)
+ .setName(deviceName)
+ .setRssi(rssi);
+ for (int i : advertisement.getActions()) {
+ builder.addExtendedProperty(new DataElement(DataElement.DataType.ACTION,
+ new byte[]{(byte) i}));
+ }
+ for (DataElement dataElement : advertisement.getDataElements()) {
+ builder.addExtendedProperty(dataElement);
+ }
+ return builder.build();
+ }
+
private static List<ScanFilter> getScanFilters() {
List<ScanFilter> scanFilterList = new ArrayList<>();
scanFilterList.add(
@@ -120,8 +173,9 @@
@Override
protected void onStart() {
if (isBleAvailable()) {
- Log.d(TAG, "BleDiscoveryProvider started.");
- startScan(getScanFilters(), getScanSettings(), mScanCallback);
+ Log.d(TAG, "BleDiscoveryProvider started");
+ startScan(getScanFilters(), getScanSettings(/* legacy= */ false), mScanCallback);
+ startScan(getScanFilters(), getScanSettings(/* legacy= */ true), mScanCallbackLegacy);
return;
}
Log.w(TAG, "Cannot start BleDiscoveryProvider because Ble is not available.");
@@ -138,6 +192,12 @@
}
Log.v(TAG, "Ble scan stopped.");
bluetoothLeScanner.stopScan(mScanCallback);
+ bluetoothLeScanner.stopScan(mScanCallbackLegacy);
+ synchronized (mLock) {
+ if (mScanFilters != null) {
+ mScanFilters = null;
+ }
+ }
}
@Override
@@ -146,6 +206,20 @@
onStart();
}
+ @Override
+ protected void onSetScanFilters(List<android.nearby.ScanFilter> filters) {
+ synchronized (mLock) {
+ mScanFilters = filters == null ? null : List.copyOf(filters);
+ }
+ }
+
+ @VisibleForTesting
+ protected List<android.nearby.ScanFilter> getFiltersLocked() {
+ synchronized (mLock) {
+ return mScanFilters == null ? null : List.copyOf(mScanFilters);
+ }
+ }
+
private void startScan(
List<ScanFilter> scanFilters, ScanSettings scanSettings,
android.bluetooth.le.ScanCallback scanCallback) {
@@ -169,7 +243,7 @@
}
}
- private ScanSettings getScanSettings() {
+ private ScanSettings getScanSettings(boolean legacy) {
int bleScanMode = ScanSettings.SCAN_MODE_LOW_POWER;
switch (mController.getProviderScanMode()) {
case ScanRequest.SCAN_MODE_LOW_LATENCY:
@@ -185,11 +259,45 @@
bleScanMode = ScanSettings.SCAN_MODE_OPPORTUNISTIC;
break;
}
- return new ScanSettings.Builder().setScanMode(bleScanMode).build();
+ return new ScanSettings.Builder().setScanMode(bleScanMode).setLegacy(legacy).build();
}
@VisibleForTesting
ScanCallback getScanCallback() {
return mScanCallback;
}
+
+ private void setPresenceDevice(byte[] data, NearbyDeviceParcelable.Builder builder,
+ String deviceName, int rssi) {
+ synchronized (mLock) {
+ if (mScanFilters == null) {
+ return;
+ }
+ for (android.nearby.ScanFilter scanFilter : mScanFilters) {
+ if (scanFilter instanceof PresenceScanFilter) {
+ // Iterate all possible authenticity key and identity combinations to decrypt
+ // advertisement
+ PresenceScanFilter presenceFilter = (PresenceScanFilter) scanFilter;
+ for (PublicCredential credential : presenceFilter.getCredentials()) {
+ ExtendedAdvertisement advertisement =
+ ExtendedAdvertisement.fromBytes(data, credential);
+ if (advertisement == null) {
+ continue;
+ }
+ if (CryptorImpIdentityV1.getInstance().verify(
+ advertisement.getIdentity(),
+ credential.getEncryptedMetadataKeyTag())) {
+ builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
+ rssi));
+ builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
+ if (!ArrayUtils.isEmpty(credential.getSecretId())) {
+ builder.setDeviceId(Arrays.hashCode(credential.getSecretId()));
+ }
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java b/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
index 5077ffe..020c7b2 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
@@ -19,15 +19,18 @@
import static com.android.server.nearby.NearbyService.TAG;
import android.annotation.Nullable;
+import android.content.Context;
import android.hardware.location.ContextHubClient;
import android.hardware.location.ContextHubClientCallback;
import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubManager;
import android.hardware.location.ContextHubTransaction;
import android.hardware.location.NanoAppMessage;
import android.hardware.location.NanoAppState;
import android.util.Log;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
import com.android.server.nearby.injector.Injector;
import com.google.common.base.Preconditions;
@@ -36,7 +39,9 @@
import java.util.List;
import java.util.Locale;
import java.util.Set;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
/**
* Responsible for setting up communication with the appropriate contexthub on the device and
@@ -44,6 +49,8 @@
*/
public class ChreCommunication extends ContextHubClientCallback {
+ public static final int INVALID_NANO_APP_VERSION = -1;
+
/** Callback that receives messages forwarded from the context hub. */
public interface ContextHubCommsCallback {
/** Indicates whether {@link ChreCommunication} was started successfully. */
@@ -63,19 +70,30 @@
}
private final Injector mInjector;
+ private final Context mContext;
private final Executor mExecutor;
private boolean mStarted = false;
+ // null when CHRE availability result has not been returned
+ @Nullable private Boolean mChreSupport = null;
+ private long mNanoAppVersion = INVALID_NANO_APP_VERSION;
@Nullable private ContextHubCommsCallback mCallback;
@Nullable private ContextHubClient mContextHubClient;
+ private CountDownLatch mCountDownLatch;
- public ChreCommunication(Injector injector, Executor executor) {
+ public ChreCommunication(Injector injector, Context context, Executor executor) {
mInjector = injector;
+ mContext = context;
mExecutor = executor;
}
- public boolean available() {
- return mContextHubClient != null;
+ /**
+ * @return {@code true} if NanoApp is available and {@code null} when CHRE availability result
+ * has not been returned
+ */
+ @Nullable
+ public Boolean available() {
+ return mChreSupport;
}
/**
@@ -86,12 +104,12 @@
* contexthub.
*/
public synchronized void start(ContextHubCommsCallback callback, Set<Long> nanoAppIds) {
- ContextHubManagerAdapter manager = mInjector.getContextHubManagerAdapter();
+ ContextHubManager manager = mInjector.getContextHubManager();
if (manager == null) {
Log.e(TAG, "ContexHub not available in this device");
return;
} else {
- Log.i(TAG, "Start ChreCommunication");
+ Log.i(TAG, "[ChreCommunication] Start ChreCommunication");
}
Preconditions.checkNotNull(callback);
Preconditions.checkArgument(!nanoAppIds.isEmpty());
@@ -134,6 +152,7 @@
if (mContextHubClient != null) {
mContextHubClient.close();
mContextHubClient = null;
+ mChreSupport = null;
}
}
@@ -156,6 +175,25 @@
return true;
}
+ /**
+ * Checks the Nano App version
+ */
+ public long queryNanoAppVersion() {
+ if (mCountDownLatch == null || mCountDownLatch.getCount() == 0) {
+ // already gets result from CHRE
+ return mNanoAppVersion;
+ }
+ try {
+ boolean success = mCountDownLatch.await(1, TimeUnit.SECONDS);
+ if (!success) {
+ Log.w(TAG, "Failed to get ContextHubTransaction result before the timeout.");
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ return mNanoAppVersion;
+ }
+
@Override
public synchronized void onMessageFromNanoApp(ContextHubClient client, NanoAppMessage message) {
mCallback.onMessageFromNanoApp(message);
@@ -172,7 +210,8 @@
mCallback.onNanoAppRestart(nanoAppId);
}
- private static String contextHubTransactionResultToString(int result) {
+ @VisibleForTesting
+ static String contextHubTransactionResultToString(int result) {
switch (result) {
case ContextHubTransaction.RESULT_SUCCESS:
return "RESULT_SUCCESS";
@@ -207,13 +246,13 @@
private final ContextHubInfo mQueriedContextHub;
private final List<ContextHubInfo> mContextHubs;
private final Set<Long> mNanoAppIds;
- private final ContextHubManagerAdapter mManager;
+ private final ContextHubManager mManager;
OnQueryCompleteListener(
ContextHubInfo queriedContextHub,
List<ContextHubInfo> contextHubs,
Set<Long> nanoAppIds,
- ContextHubManagerAdapter manager) {
+ ContextHubManager manager) {
this.mQueriedContextHub = queriedContextHub;
this.mContextHubs = contextHubs;
this.mNanoAppIds = nanoAppIds;
@@ -231,21 +270,32 @@
return;
}
+ mCountDownLatch = new CountDownLatch(1);
if (response.getResult() == ContextHubTransaction.RESULT_SUCCESS) {
for (NanoAppState state : response.getContents()) {
+ long version = state.getNanoAppVersion();
+ NearbyConfiguration configuration = new NearbyConfiguration();
+ long minVersion = configuration.getNanoAppMinVersion();
+ if (version < minVersion) {
+ Log.w(TAG, String.format("Current nano app version is %s, which does not "
+ + "meet minimum version required %s", version, minVersion));
+ continue;
+ }
if (mNanoAppIds.contains(state.getNanoAppId())) {
Log.i(
TAG,
String.format(
"Found valid contexthub: %s", mQueriedContextHub.getId()));
- mContextHubClient =
- mManager.createClient(
- mQueriedContextHub, ChreCommunication.this, mExecutor);
+ mContextHubClient = mManager.createClient(mContext, mQueriedContextHub,
+ mExecutor, ChreCommunication.this);
+ mChreSupport = true;
mCallback.started(true);
+ mNanoAppVersion = version;
+ mCountDownLatch.countDown();
return;
}
}
- Log.e(
+ Log.i(
TAG,
String.format(
"Didn't find the nanoapp on contexthub: %s",
@@ -259,10 +309,12 @@
}
mContextHubs.remove(mQueriedContextHub);
+ mCountDownLatch.countDown();
// If this is the last context hub response left to receive, indicate that
// there isn't a valid context available on this device.
if (mContextHubs.isEmpty()) {
mCallback.started(false);
+ mChreSupport = false;
}
}
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
index f20c6d8..7ab0523 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -20,20 +20,33 @@
import static com.android.server.nearby.NearbyService.TAG;
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.hardware.location.NanoAppMessage;
+import android.nearby.DataElement;
import android.nearby.NearbyDevice;
import android.nearby.NearbyDeviceParcelable;
+import android.nearby.OffloadCapability;
+import android.nearby.PresenceDevice;
import android.nearby.PresenceScanFilter;
import android.nearby.PublicCredential;
import android.nearby.ScanFilter;
+import android.nearby.aidl.IOffloadCallback;
+import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
-import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.ByteString;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
import java.util.concurrent.Executor;
import service.proto.Blefilter;
@@ -42,52 +55,122 @@
public class ChreDiscoveryProvider extends AbstractDiscoveryProvider {
// Nanoapp ID reserved for Nearby Presence.
/** @hide */
- @VisibleForTesting public static final long NANOAPP_ID = 0x476f6f676c001031L;
+ @VisibleForTesting
+ public static final long NANOAPP_ID = 0x476f6f676c001031L;
/** @hide */
- @VisibleForTesting public static final int NANOAPP_MESSAGE_TYPE_FILTER = 3;
+ @VisibleForTesting
+ public static final int NANOAPP_MESSAGE_TYPE_FILTER = 3;
/** @hide */
- @VisibleForTesting public static final int NANOAPP_MESSAGE_TYPE_FILTER_RESULT = 4;
+ @VisibleForTesting
+ public static final int NANOAPP_MESSAGE_TYPE_FILTER_RESULT = 4;
+ /** @hide */
+ @VisibleForTesting
+ public static final int NANOAPP_MESSAGE_TYPE_CONFIG = 5;
- private static final int PRESENCE_UUID = 0xFCF1;
+ private final ChreCommunication mChreCommunication;
+ private final ChreCallback mChreCallback;
+ private final Object mLock = new Object();
- private ChreCommunication mChreCommunication;
- private ChreCallback mChreCallback;
private boolean mChreStarted = false;
- private Blefilter.BleFilters mFilters = null;
- private int mFilterId;
+ private Context mContext;
+ private NearbyConfiguration mNearbyConfiguration;
+ private final IntentFilter mIntentFilter;
+ // Null when CHRE not started and the filters are never set. Empty the list every time the scan
+ // stops.
+ @GuardedBy("mLock")
+ @Nullable
+ private List<ScanFilter> mScanFilters;
+
+ private final BroadcastReceiver mScreenBroadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Boolean screenOn = intent.getAction().equals(Intent.ACTION_SCREEN_ON)
+ || intent.getAction().equals(Intent.ACTION_USER_PRESENT);
+ Log.d(TAG, String.format(
+ "[ChreDiscoveryProvider] update nanoapp screen status: %B", screenOn));
+ sendScreenUpdate(screenOn);
+ }
+ };
public ChreDiscoveryProvider(
Context context, ChreCommunication chreCommunication, Executor executor) {
super(context, executor);
+ mContext = context;
mChreCommunication = chreCommunication;
mChreCallback = new ChreCallback();
- mFilterId = 0;
+ mIntentFilter = new IntentFilter();
+ }
+
+ /** Initialize the CHRE discovery provider. */
+ public void init() {
+ mChreCommunication.start(mChreCallback, Collections.singleton(NANOAPP_ID));
+ mNearbyConfiguration = new NearbyConfiguration();
}
@Override
protected void onStart() {
Log.d(TAG, "Start CHRE scan");
- mChreCommunication.start(mChreCallback, Collections.singleton(NANOAPP_ID));
- updateFilters();
+ synchronized (mLock) {
+ updateFiltersLocked();
+ }
}
@Override
protected void onStop() {
- mChreStarted = false;
- mChreCommunication.stop();
+ Log.d(TAG, "Stop CHRE scan");
+ synchronized (mLock) {
+ if (mScanFilters != null) {
+ // Cleaning the filters by assigning an empty list
+ mScanFilters = List.of();
+ }
+ updateFiltersLocked();
+ }
}
@Override
- protected void invalidateScanMode() {
- onStop();
- onStart();
+ protected void onSetScanFilters(List<ScanFilter> filters) {
+ synchronized (mLock) {
+ mScanFilters = filters == null ? null : List.copyOf(filters);
+ updateFiltersLocked();
+ }
}
- public boolean available() {
+ /**
+ * @return {@code true} if CHRE is available and {@code null} when CHRE availability result
+ * has not been returned
+ */
+ @Nullable
+ public Boolean available() {
return mChreCommunication.available();
}
- private synchronized void updateFilters() {
+ /**
+ * Query offload capability in a device.
+ */
+ public void queryOffloadCapability(IOffloadCallback callback) {
+ OffloadCapability.Builder builder = new OffloadCapability.Builder();
+ mExecutor.execute(() -> {
+ long version = mChreCommunication.queryNanoAppVersion();
+ builder.setVersion(version);
+ builder.setFastPairSupported(version != ChreCommunication.INVALID_NANO_APP_VERSION);
+ try {
+ callback.onQueryComplete(builder.build());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ @VisibleForTesting
+ public List<ScanFilter> getFiltersLocked() {
+ synchronized (mLock) {
+ return mScanFilters == null ? null : List.copyOf(mScanFilters);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void updateFiltersLocked() {
if (mScanFilters == null) {
Log.e(TAG, "ScanFilters not set.");
return;
@@ -95,29 +178,69 @@
Blefilter.BleFilters.Builder filtersBuilder = Blefilter.BleFilters.newBuilder();
for (ScanFilter scanFilter : mScanFilters) {
PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
- Blefilter.BleFilter filter =
- Blefilter.BleFilter.newBuilder()
- .setId(mFilterId)
- .setUuid(PRESENCE_UUID)
- .setIntent(presenceScanFilter.getPresenceActions().get(0))
- .build();
- filtersBuilder.addFilter(filter);
- mFilterId++;
+ Blefilter.BleFilter.Builder filterBuilder = Blefilter.BleFilter.newBuilder();
+ for (PublicCredential credential : presenceScanFilter.getCredentials()) {
+ filterBuilder.addCertificate(toProtoPublicCredential(credential));
+ }
+ for (DataElement dataElement : presenceScanFilter.getExtendedProperties()) {
+ if (dataElement.getKey() == DataElement.DataType.ACCOUNT_KEY_DATA) {
+ filterBuilder.addDataElement(toProtoDataElement(dataElement));
+ } else if (mNearbyConfiguration.isTestAppSupported()
+ && DataElement.isTestDeType(dataElement.getKey())) {
+ filterBuilder.addDataElement(toProtoDataElement(dataElement));
+ }
+ }
+ if (!presenceScanFilter.getPresenceActions().isEmpty()) {
+ filterBuilder.setIntent(presenceScanFilter.getPresenceActions().get(0));
+ }
+ filtersBuilder.addFilter(filterBuilder.build());
}
- mFilters = filtersBuilder.build();
if (mChreStarted) {
- sendFilters(mFilters);
- mFilters = null;
+ sendFilters(filtersBuilder.build());
}
}
+ private Blefilter.PublicateCertificate toProtoPublicCredential(PublicCredential credential) {
+ Log.d(TAG, String.format("Returns a PublicCertificate with authenticity key size %d and"
+ + " encrypted metadata key tag size %d",
+ credential.getAuthenticityKey().length,
+ credential.getEncryptedMetadataKeyTag().length));
+ return Blefilter.PublicateCertificate.newBuilder()
+ .setAuthenticityKey(ByteString.copyFrom(credential.getAuthenticityKey()))
+ .setMetadataEncryptionKeyTag(
+ ByteString.copyFrom(credential.getEncryptedMetadataKeyTag()))
+ .build();
+ }
+
+ private Blefilter.DataElement toProtoDataElement(DataElement dataElement) {
+ return Blefilter.DataElement.newBuilder()
+ .setKey(dataElement.getKey())
+ .setValue(ByteString.copyFrom(dataElement.getValue()))
+ .setValueLength(dataElement.getValue().length)
+ .build();
+ }
+
private void sendFilters(Blefilter.BleFilters filters) {
NanoAppMessage message =
NanoAppMessage.createMessageToNanoApp(
NANOAPP_ID, NANOAPP_MESSAGE_TYPE_FILTER, filters.toByteArray());
- if (!mChreCommunication.sendMessageToNanoApp(message)) {
- Log.e(TAG, "Failed to send filters to CHRE.");
+ if (mChreCommunication.sendMessageToNanoApp(message)) {
+ Log.v(TAG, "Successfully sent filters to CHRE.");
+ return;
}
+ Log.e(TAG, "Failed to send filters to CHRE.");
+ }
+
+ private void sendScreenUpdate(Boolean screenOn) {
+ Blefilter.BleConfig config = Blefilter.BleConfig.newBuilder().setScreenOn(screenOn).build();
+ NanoAppMessage message =
+ NanoAppMessage.createMessageToNanoApp(
+ NANOAPP_ID, NANOAPP_MESSAGE_TYPE_CONFIG, config.toByteArray());
+ if (mChreCommunication.sendMessageToNanoApp(message)) {
+ Log.v(TAG, "Successfully sent config to CHRE.");
+ return;
+ }
+ Log.e(TAG, "Failed to send config to CHRE.");
}
private class ChreCallback implements ChreCommunication.ContextHubCommsCallback {
@@ -127,11 +250,11 @@
if (success) {
synchronized (ChreDiscoveryProvider.this) {
Log.i(TAG, "CHRE communication started");
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
+ mIntentFilter.addAction(Intent.ACTION_USER_PRESENT);
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ mContext.registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
mChreStarted = true;
- if (mFilters != null) {
- sendFilters(mFilters);
- mFilters = null;
- }
}
}
}
@@ -163,32 +286,123 @@
Blefilter.BleFilterResults results =
Blefilter.BleFilterResults.parseFrom(message.getMessageBody());
for (Blefilter.BleFilterResult filterResult : results.getResultList()) {
- Blefilter.PublicCredential credential = filterResult.getPublicCredential();
+ // TODO(b/234653356): There are some duplicate fields set both in
+ // PresenceDevice and NearbyDeviceParcelable, cleanup is needed.
+ byte[] salt = {1};
+ byte[] secretId = {1};
+ byte[] authenticityKey = {1};
+ byte[] publicKey = {1};
+ byte[] encryptedMetaData = {1};
+ byte[] encryptedMetaDataTag = {1};
+ if (filterResult.hasPublicCredential()) {
+ Blefilter.PublicCredential credential =
+ filterResult.getPublicCredential();
+ secretId = credential.getSecretId().toByteArray();
+ authenticityKey = credential.getAuthenticityKey().toByteArray();
+ publicKey = credential.getPublicKey().toByteArray();
+ encryptedMetaData = credential.getEncryptedMetadata().toByteArray();
+ encryptedMetaDataTag =
+ credential.getEncryptedMetadataTag().toByteArray();
+ }
+ PresenceDevice.Builder presenceDeviceBuilder =
+ new PresenceDevice.Builder(
+ String.valueOf(filterResult.hashCode()),
+ salt,
+ secretId,
+ encryptedMetaData)
+ .setRssi(filterResult.getRssi())
+ .addMedium(NearbyDevice.Medium.BLE);
+ // Data Elements reported from nanoapp added to Data Elements.
+ // i.e. Fast Pair account keys, connection status and battery
+ for (Blefilter.DataElement element : filterResult.getDataElementList()) {
+ addDataElementsToPresenceDevice(element, presenceDeviceBuilder);
+ }
+ // BlE address appended to Data Element.
+ if (filterResult.hasBluetoothAddress()) {
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(
+ DataElement.DataType.BLE_ADDRESS,
+ filterResult.getBluetoothAddress().toByteArray()));
+ }
+ // BlE TX Power appended to Data Element.
+ if (filterResult.hasTxPower()) {
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(
+ DataElement.DataType.TX_POWER,
+ new byte[]{(byte) filterResult.getTxPower()}));
+ }
+ // BLE Service data appended to Data Elements.
+ if (filterResult.hasBleServiceData()) {
+ // Retrieves the length of the service data from the first byte,
+ // and then skips the first byte and returns data[1 .. dataLength)
+ // as the DataElement value.
+ int dataLength = Byte.toUnsignedInt(
+ filterResult.getBleServiceData().byteAt(0));
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(
+ DataElement.DataType.BLE_SERVICE_DATA,
+ filterResult.getBleServiceData()
+ .substring(1, 1 + dataLength).toByteArray()));
+ }
+ // Add action
+ if (filterResult.hasIntent()) {
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(
+ DataElement.DataType.ACTION,
+ new byte[]{(byte) filterResult.getIntent()}));
+ }
+
PublicCredential publicCredential =
new PublicCredential.Builder(
- credential.getSecretId().toByteArray(),
- credential.getAuthenticityKey().toByteArray(),
- credential.getPublicKey().toByteArray(),
- credential.getEncryptedMetadata().toByteArray(),
- credential.getEncryptedMetadataTag().toByteArray())
+ secretId,
+ authenticityKey,
+ publicKey,
+ encryptedMetaData,
+ encryptedMetaDataTag)
.build();
+
NearbyDeviceParcelable device =
new NearbyDeviceParcelable.Builder()
+ .setDeviceId(Arrays.hashCode(secretId))
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.setMedium(NearbyDevice.Medium.BLE)
.setTxPower(filterResult.getTxPower())
.setRssi(filterResult.getRssi())
.setAction(filterResult.getIntent())
.setPublicCredential(publicCredential)
+ .setPresenceDevice(presenceDeviceBuilder.build())
+ .setEncryptionKeyTag(encryptedMetaDataTag)
.build();
mExecutor.execute(() -> mListener.onNearbyDeviceDiscovered(device));
}
- } catch (InvalidProtocolBufferException e) {
- Log.e(
- TAG,
- String.format("Failed to decode the filter result %s", e.toString()));
+ } catch (Exception e) {
+ Log.e(TAG, String.format("Failed to decode the filter result %s", e));
}
}
}
+
+ private void addDataElementsToPresenceDevice(Blefilter.DataElement element,
+ PresenceDevice.Builder presenceDeviceBuilder) {
+ int endIndex = element.hasValueLength() ? element.getValueLength() :
+ element.getValue().size();
+ int key = element.getKey();
+ switch (key) {
+ case DataElement.DataType.ACCOUNT_KEY_DATA:
+ case DataElement.DataType.CONNECTION_STATUS:
+ case DataElement.DataType.BATTERY:
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(key,
+ element.getValue().substring(0, endIndex).toByteArray()));
+ break;
+ default:
+ if (mNearbyConfiguration.isTestAppSupported()
+ && DataElement.isTestDeType(key)) {
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(key,
+ element.getValue().substring(0, endIndex).toByteArray()));
+ }
+ break;
+ }
+ }
}
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java
index fa1a874..71ffda5 100644
--- a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java
+++ b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java
@@ -23,7 +23,7 @@
import java.util.List;
/** Interface for controlling discovery providers. */
-interface DiscoveryProviderController {
+public interface DiscoveryProviderController {
/**
* Sets the listener which can expect to receive all state updates from after this point. May be
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
deleted file mode 100644
index bdeab51..0000000
--- a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
+++ /dev/null
@@ -1,332 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * 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.
- */
-
-package com.android.server.nearby.provider;
-
-import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
-
-import static com.android.server.nearby.NearbyService.TAG;
-
-import android.annotation.Nullable;
-import android.app.AppOpsManager;
-import android.content.Context;
-import android.nearby.IScanListener;
-import android.nearby.NearbyDeviceParcelable;
-import android.nearby.PresenceScanFilter;
-import android.nearby.ScanFilter;
-import android.nearby.ScanRequest;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.server.nearby.injector.Injector;
-import com.android.server.nearby.metrics.NearbyMetrics;
-import com.android.server.nearby.presence.PresenceDiscoveryResult;
-import com.android.server.nearby.util.identity.CallerIdentity;
-import com.android.server.nearby.util.permissions.DiscoveryPermissions;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.stream.Collectors;
-
-/** Manages all aspects of discovery providers. */
-public class DiscoveryProviderManager implements AbstractDiscoveryProvider.Listener {
-
- protected final Object mLock = new Object();
- private final Context mContext;
- private final BleDiscoveryProvider mBleDiscoveryProvider;
- @Nullable private final ChreDiscoveryProvider mChreDiscoveryProvider;
- private @ScanRequest.ScanMode int mScanMode;
- private final Injector mInjector;
-
- @GuardedBy("mLock")
- private Map<IBinder, ScanListenerRecord> mScanTypeScanListenerRecordMap;
-
- @Override
- public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
- synchronized (mLock) {
- AppOpsManager appOpsManager = Objects.requireNonNull(mInjector.getAppOpsManager());
- for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
- ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
- if (record == null) {
- Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
- continue;
- }
- CallerIdentity callerIdentity = record.getCallerIdentity();
- if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
- appOpsManager, callerIdentity)) {
- Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
- + "- not forwarding results");
- try {
- record.getScanListener().onError();
- } catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
- }
- return;
- }
-
- if (nearbyDevice.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
- List<ScanFilter> presenceFilters =
- record.getScanRequest().getScanFilters().stream()
- .filter(
- scanFilter ->
- scanFilter.getType()
- == SCAN_TYPE_NEARBY_PRESENCE)
- .collect(Collectors.toList());
- Log.i(
- TAG,
- String.format("match with filters size: %d", presenceFilters.size()));
- if (!presenceFilterMatches(nearbyDevice, presenceFilters)) {
- continue;
- }
- }
- try {
- record.getScanListener()
- .onDiscovered(
- PrivacyFilter.filter(
- record.getScanRequest().getScanType(), nearbyDevice));
- NearbyMetrics.logScanDeviceDiscovered(
- record.hashCode(), record.getScanRequest(), nearbyDevice);
- } catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
- }
- }
- }
- }
-
- public DiscoveryProviderManager(Context context, Injector injector) {
- mContext = context;
- mBleDiscoveryProvider = new BleDiscoveryProvider(mContext, injector);
- Executor executor = Executors.newSingleThreadExecutor();
- mChreDiscoveryProvider =
- new ChreDiscoveryProvider(
- mContext, new ChreCommunication(injector, executor), executor);
- mScanTypeScanListenerRecordMap = new HashMap<>();
- mInjector = injector;
- }
-
- /**
- * Registers the listener in the manager and starts scan according to the requested scan mode.
- */
- public boolean registerScanListener(ScanRequest scanRequest, IScanListener listener,
- CallerIdentity callerIdentity) {
- synchronized (mLock) {
- IBinder listenerBinder = listener.asBinder();
- if (mScanTypeScanListenerRecordMap.containsKey(listener.asBinder())) {
- ScanRequest savedScanRequest =
- mScanTypeScanListenerRecordMap.get(listenerBinder).getScanRequest();
- if (scanRequest.equals(savedScanRequest)) {
- Log.d(TAG, "Already registered the scanRequest: " + scanRequest);
- return true;
- }
- }
- ScanListenerRecord scanListenerRecord =
- new ScanListenerRecord(scanRequest, listener, callerIdentity);
- mScanTypeScanListenerRecordMap.put(listenerBinder, scanListenerRecord);
-
- if (!startProviders(scanRequest)) {
- return false;
- }
-
- NearbyMetrics.logScanStarted(scanListenerRecord.hashCode(), scanRequest);
- if (mScanMode < scanRequest.getScanMode()) {
- mScanMode = scanRequest.getScanMode();
- invalidateProviderScanMode();
- }
- return true;
- }
- }
-
- /**
- * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
- */
- public void unregisterScanListener(IScanListener listener) {
- IBinder listenerBinder = listener.asBinder();
- synchronized (mLock) {
- if (!mScanTypeScanListenerRecordMap.containsKey(listenerBinder)) {
- Log.w(
- TAG,
- "Cannot unregister the scanRequest because the request is never "
- + "registered.");
- return;
- }
-
- ScanListenerRecord removedRecord =
- mScanTypeScanListenerRecordMap.remove(listenerBinder);
- Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
- NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
- if (mScanTypeScanListenerRecordMap.isEmpty()) {
- Log.v(TAG, "DiscoveryProviderManager stops provider because there is no "
- + "scan listener registered.");
- stopProviders();
- return;
- }
-
- // TODO(b/221082271): updates the scan with reduced filters.
-
- // Removes current highest scan mode requested and sets the next highest scan mode.
- if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
- Log.v(TAG, "DiscoveryProviderManager starts to find the new highest scan mode "
- + "because the highest scan mode listener was unregistered.");
- @ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
- // find the next highest scan mode;
- for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
- @ScanRequest.ScanMode int scanMode = record.getScanRequest().getScanMode();
- if (scanMode > highestScanModeRequested) {
- highestScanModeRequested = scanMode;
- }
- }
- if (mScanMode != highestScanModeRequested) {
- mScanMode = highestScanModeRequested;
- invalidateProviderScanMode();
- }
- }
- }
- }
-
- // Returns false when fail to start all the providers. Returns true if any one of the provider
- // starts successfully.
- private boolean startProviders(ScanRequest scanRequest) {
- if (scanRequest.isBleEnabled()) {
- if (mChreDiscoveryProvider.available()
- && scanRequest.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
- startChreProvider();
- } else {
- startBleProvider(scanRequest);
- }
- return true;
- }
- return false;
- }
-
- private void startBleProvider(ScanRequest scanRequest) {
- if (!mBleDiscoveryProvider.getController().isStarted()) {
- Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
- mBleDiscoveryProvider.getController().start();
- mBleDiscoveryProvider.getController().setListener(this);
- mBleDiscoveryProvider.getController().setProviderScanMode(scanRequest.getScanMode());
- }
- }
-
- private void startChreProvider() {
- Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
- synchronized (mLock) {
- mChreDiscoveryProvider.getController().setListener(this);
- List<ScanFilter> scanFilters = new ArrayList();
- for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
- ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
- List<ScanFilter> presenceFilters =
- record.getScanRequest().getScanFilters().stream()
- .filter(
- scanFilter ->
- scanFilter.getType() == SCAN_TYPE_NEARBY_PRESENCE)
- .collect(Collectors.toList());
- scanFilters.addAll(presenceFilters);
- }
- mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
- mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
- mChreDiscoveryProvider.getController().start();
- }
- }
-
- private void stopProviders() {
- stopBleProvider();
- stopChreProvider();
- }
-
- private void stopBleProvider() {
- mBleDiscoveryProvider.getController().stop();
- }
-
- private void stopChreProvider() {
- mChreDiscoveryProvider.getController().stop();
- }
-
- private void invalidateProviderScanMode() {
- if (mBleDiscoveryProvider.getController().isStarted()) {
- mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
- } else {
- Log.d(
- TAG,
- "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
- + "started.");
- }
- }
-
- private static boolean presenceFilterMatches(
- NearbyDeviceParcelable device, List<ScanFilter> scanFilters) {
- if (scanFilters.isEmpty()) {
- return true;
- }
- PresenceDiscoveryResult discoveryResult = PresenceDiscoveryResult.fromDevice(device);
- for (ScanFilter scanFilter : scanFilters) {
- PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
- if (discoveryResult.matches(presenceScanFilter)) {
- return true;
- }
- }
- return false;
- }
-
- private static class ScanListenerRecord {
-
- private final ScanRequest mScanRequest;
-
- private final IScanListener mScanListener;
-
- private final CallerIdentity mCallerIdentity;
-
- ScanListenerRecord(ScanRequest scanRequest, IScanListener iScanListener,
- CallerIdentity callerIdentity) {
- mScanListener = iScanListener;
- mScanRequest = scanRequest;
- mCallerIdentity = callerIdentity;
- }
-
- IScanListener getScanListener() {
- return mScanListener;
- }
-
- ScanRequest getScanRequest() {
- return mScanRequest;
- }
-
- CallerIdentity getCallerIdentity() {
- return mCallerIdentity;
- }
-
- @Override
- public boolean equals(Object other) {
- if (other instanceof ScanListenerRecord) {
- ScanListenerRecord otherScanListenerRecord = (ScanListenerRecord) other;
- return Objects.equals(mScanRequest, otherScanListenerRecord.mScanRequest)
- && Objects.equals(mScanListener, otherScanListenerRecord.mScanListener);
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mScanListener, mScanRequest);
- }
- }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
index 599843c..35251d8 100644
--- a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
+++ b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
@@ -45,4 +45,11 @@
}
return result;
}
+
+ /**
+ * @return true when the array is null or length is 0
+ */
+ public static boolean isEmpty(byte[] bytes) {
+ return bytes == null || bytes.length == 0;
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
new file mode 100644
index 0000000..3c5132d
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/** Class for encryption/decryption functionality. */
+public abstract class Cryptor {
+
+ /** AES only supports key sizes of 16, 24 or 32 bytes. */
+ static final int AUTHENTICITY_KEY_BYTE_SIZE = 16;
+
+ private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
+
+ /**
+ * Encrypt the provided data blob.
+ *
+ * @param data data blob to be encrypted.
+ * @param salt used for IV
+ * @param secretKeyBytes secrete key accessed from credentials
+ * @return encrypted data, {@code null} if failed to encrypt.
+ */
+ @Nullable
+ public byte[] encrypt(byte[] data, byte[] salt, byte[] secretKeyBytes) {
+ return data;
+ }
+
+ /**
+ * Decrypt the original data blob from the provided byte array.
+ *
+ * @param encryptedData data blob to be decrypted.
+ * @param salt used for IV
+ * @param secretKeyBytes secrete key accessed from credentials
+ * @return decrypted data, {@code null} if failed to decrypt.
+ */
+ @Nullable
+ public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] secretKeyBytes) {
+ return encryptedData;
+ }
+
+ /**
+ * Generates a digital signature for the data.
+ *
+ * @return signature {@code null} if failed to sign
+ */
+ @Nullable
+ public byte[] sign(byte[] data, byte[] key) {
+ return new byte[0];
+ }
+
+ /**
+ * Verifies the signature generated by data and key, with the original signed data
+ */
+ public boolean verify(byte[] data, byte[] key, byte[] signature) {
+ return true;
+ }
+
+ /**
+ * @return length of the signature generated
+ */
+ public int getSignatureLength() {
+ return 0;
+ }
+
+ /**
+ * A HAMC sha256 based HKDF algorithm to pseudo randomly hash data and salt into a byte array of
+ * given size.
+ */
+ // Based on google3/third_party/tink/java/src/main/java/com/google/crypto/tink/subtle/Hkdf.java
+ @Nullable
+ static byte[] computeHkdf(byte[] ikm, byte[] salt, int size) {
+ Mac mac;
+ try {
+ mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
+ } catch (NoSuchAlgorithmException e) {
+ Log.w(TAG, "HMAC_SHA256_ALGORITHM is not supported.", e);
+ return null;
+ }
+
+ if (size > 255 * mac.getMacLength()) {
+ Log.w(TAG, "Size too large.");
+ return null;
+ }
+
+ if (salt.length == 0) {
+ Log.w(TAG, "Salt cannot be empty.");
+ return null;
+ }
+
+ try {
+ mac.init(new SecretKeySpec(salt, HMAC_SHA256_ALGORITHM));
+ } catch (InvalidKeyException e) {
+ Log.w(TAG, "Invalid key.", e);
+ return null;
+ }
+
+ byte[] prk = mac.doFinal(ikm);
+ byte[] result = new byte[size];
+ try {
+ mac.init(new SecretKeySpec(prk, HMAC_SHA256_ALGORITHM));
+ } catch (InvalidKeyException e) {
+ Log.w(TAG, "Invalid key.", e);
+ return null;
+ }
+
+ byte[] digest = new byte[0];
+ int ctr = 1;
+ int pos = 0;
+ while (true) {
+ mac.update(digest);
+ mac.update((byte) ctr);
+ digest = mac.doFinal();
+ if (pos + digest.length < size) {
+ System.arraycopy(digest, 0, result, pos, digest.length);
+ pos += digest.length;
+ ctr++;
+ } else {
+ System.arraycopy(digest, 0, result, pos, size - pos);
+ break;
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
new file mode 100644
index 0000000..1c0ec9e
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A Cryptor that returns the original data without actual encryption
+ */
+public class CryptorImpFake extends Cryptor {
+ // Lazily instantiated when {@link #getInstance()} is called.
+ @Nullable
+ private static CryptorImpFake sCryptor;
+
+ /** Returns an instance of CryptorImpFake. */
+ public static CryptorImpFake getInstance() {
+ if (sCryptor == null) {
+ sCryptor = new CryptorImpFake();
+ }
+ return sCryptor;
+ }
+
+ private CryptorImpFake() {
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
new file mode 100644
index 0000000..b0e19b4
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for identity
+ * encryption and decryption.
+ */
+public class CryptorImpIdentityV1 extends Cryptor {
+
+ // 3 16 byte arrays known by both the encryptor and decryptor.
+ private static final byte[] EK_IV =
+ new byte[] {14, -123, -39, 42, 109, 127, 83, 27, 27, 11, 91, -38, 92, 17, -84, 66};
+ private static final byte[] ESALT_IV =
+ new byte[] {46, 83, -19, 10, -127, -31, -31, 12, 31, 76, 63, -9, 33, -66, 15, -10};
+ private static final byte[] KTAG_IV =
+ {-22, -83, -6, 67, 16, -99, -13, -9, 8, -3, -16, 37, -75, 47, 1, -56};
+
+ /** Length of encryption key required by AES/GCM encryption. */
+ private static final int ENCRYPTION_KEY_SIZE = 32;
+
+ /** Length of salt required by AES/GCM encryption. */
+ private static final int AES_CTR_IV_SIZE = 16;
+
+ /** Length HMAC tag */
+ public static final int HMAC_TAG_SIZE = 8;
+
+ /**
+ * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
+ */
+ private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
+
+ @VisibleForTesting
+ static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
+
+ // Lazily instantiated when {@link #getInstance()} is called.
+ @Nullable private static CryptorImpIdentityV1 sCryptor;
+
+ /** Returns an instance of CryptorImpIdentityV1. */
+ public static CryptorImpIdentityV1 getInstance() {
+ if (sCryptor == null) {
+ sCryptor = new CryptorImpIdentityV1();
+ }
+ return sCryptor;
+ }
+
+ @Nullable
+ @Override
+ public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.w(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes encryption key from authenticity_key
+ byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
+ if (encryptionKey == null) {
+ Log.e(TAG, "Failed to generate encryption key.");
+ return null;
+ }
+
+ // Encrypts the data using the encryption key
+ SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+ byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
+ if (esalt == null) {
+ Log.e(TAG, "Failed to generate salt.");
+ return null;
+ }
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(esalt));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+ }
+ try {
+ return cipher.doFinal(data);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.w(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes encryption key from authenticity_key
+ byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
+ if (encryptionKey == null) {
+ Log.e(TAG, "Failed to generate encryption key.");
+ return null;
+ }
+
+ // Decrypts the data using the encryption key
+ SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to get cipher instance.", e);
+ return null;
+ }
+ byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
+ if (esalt == null) {
+ return null;
+ }
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(esalt));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+ }
+
+ try {
+ return cipher.doFinal(encryptedData);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
+ return null;
+ }
+ }
+
+ /**
+ * Generates a digital signature for the data.
+ *
+ * @return signature {@code null} if failed to sign
+ */
+ @Nullable
+ @Override
+ public byte[] sign(byte[] data, byte[] salt) {
+ if (data == null) {
+ Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
+ return null;
+ }
+
+ // Generates a 8 bytes HMAC tag
+ return Cryptor.computeHkdf(data, salt, HMAC_TAG_SIZE);
+ }
+
+ /**
+ * Generates a digital signature for the data.
+ * Uses KTAG_IV as salt value.
+ */
+ @Nullable
+ public byte[] sign(byte[] data) {
+ // Generates a 8 bytes HMAC tag
+ return sign(data, KTAG_IV);
+ }
+
+ @Override
+ public boolean verify(byte[] data, byte[] key, byte[] signature) {
+ return Arrays.equals(sign(data, key), signature);
+ }
+
+ /**
+ * Verifies the signature generated by data and key, with the original signed data. Uses
+ * KTAG_IV as salt value.
+ */
+ public boolean verify(byte[] data, byte[] signature) {
+ return verify(data, KTAG_IV, signature);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
new file mode 100644
index 0000000..15073fb
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for encryption and decryption.
+ */
+public class CryptorImpV1 extends Cryptor {
+
+ /**
+ * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
+ */
+ private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
+
+ @VisibleForTesting
+ static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
+
+ /** Length of encryption key required by AES/GCM encryption. */
+ private static final int ENCRYPTION_KEY_SIZE = 32;
+
+ /** Length of salt required by AES/GCM encryption. */
+ private static final int AES_CTR_IV_SIZE = 16;
+
+ /** Length HMAC tag */
+ public static final int HMAC_TAG_SIZE = 16;
+
+ // 3 16 byte arrays known by both the encryptor and decryptor.
+ private static final byte[] AK_IV =
+ new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
+ private static final byte[] ASALT_IV =
+ new byte[] {111, 48, -83, -79, -10, -102, -16, 73, 43, 55, 102, -127, 58, -19, -113, 4};
+ private static final byte[] HK_IV =
+ new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
+
+ // Lazily instantiated when {@link #getInstance()} is called.
+ @Nullable private static CryptorImpV1 sCryptor;
+
+ /** Returns an instance of CryptorImpV1. */
+ public static CryptorImpV1 getInstance() {
+ if (sCryptor == null) {
+ sCryptor = new CryptorImpV1();
+ }
+ return sCryptor;
+ }
+
+ private CryptorImpV1() {
+ }
+
+ @Nullable
+ @Override
+ public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.w(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes encryption key from authenticity_key
+ byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
+ if (encryptionKey == null) {
+ Log.e(TAG, "Failed to generate encryption key.");
+ return null;
+ }
+
+ // Encrypts the data using the encryption key
+ SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+ byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
+ if (asalt == null) {
+ Log.e(TAG, "Failed to generate salt.");
+ return null;
+ }
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(asalt));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+ }
+ try {
+ return cipher.doFinal(data);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.w(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes encryption key from authenticity_key
+ byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
+ if (encryptionKey == null) {
+ Log.e(TAG, "Failed to generate encryption key.");
+ return null;
+ }
+
+ // Decrypts the data using the encryption key
+ SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to get cipher instance.", e);
+ return null;
+ }
+ byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
+ if (asalt == null) {
+ return null;
+ }
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(asalt));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+ }
+
+ try {
+ return cipher.doFinal(encryptedData);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
+ return null;
+ }
+ }
+
+ @Override
+ @Nullable
+ public byte[] sign(byte[] data, byte[] key) {
+ return generateHmacTag(data, key);
+ }
+
+ @Override
+ public int getSignatureLength() {
+ return HMAC_TAG_SIZE;
+ }
+
+ @Override
+ public boolean verify(byte[] data, byte[] key, byte[] signature) {
+ return Arrays.equals(sign(data, key), signature);
+ }
+
+ /** Generates a 16 bytes HMAC tag. This is used for decryptor to verify if the computed HMAC tag
+ * is equal to HMAC tag in advertisement to see data integrity. */
+ @Nullable
+ @VisibleForTesting
+ byte[] generateHmacTag(byte[] data, byte[] authenticityKey) {
+ if (data == null || authenticityKey == null) {
+ Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
+ return null;
+ }
+
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.e(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes HMAC key from authenticity_key
+ byte[] hmacKey = Cryptor.computeHkdf(authenticityKey, HK_IV, AES_CTR_IV_SIZE);
+ if (hmacKey == null) {
+ Log.e(TAG, "Failed to generate HMAC key.");
+ return null;
+ }
+
+ // Generates a 16 bytes HMAC tag from authenticity_key
+ return Cryptor.computeHkdf(data, hmacKey, HMAC_TAG_SIZE);
+ }
+}
diff --git a/nearby/service/lint-baseline.xml b/nearby/service/lint-baseline.xml
new file mode 100644
index 0000000..a4761ab
--- /dev/null
+++ b/nearby/service/lint-baseline.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.hardware.location.ContextHubManager#createClient`"
+ errorLine1=" mContextHubClient = mManager.createClient(mContext, mQueriedContextHub,"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java"
+ line="263"
+ column="54"/>
+ </issue>
+
+</issues>
\ No newline at end of file
diff --git a/nearby/service/proto/src/presence/blefilter.proto b/nearby/service/proto/src/presence/blefilter.proto
index 9f75d34..e1bf455 100644
--- a/nearby/service/proto/src/presence/blefilter.proto
+++ b/nearby/service/proto/src/presence/blefilter.proto
@@ -47,6 +47,7 @@
optional bytes metadata_encryption_key_tag = 2;
}
+// Public credential returned in BleFilterResult.
message PublicCredential {
optional bytes secret_id = 1;
optional bytes authenticity_key = 2;
@@ -55,6 +56,23 @@
optional bytes encrypted_metadata_tag = 5;
}
+message DataElement {
+ enum ElementType {
+ DE_NONE = 0;
+ DE_FAST_PAIR_ACCOUNT_KEY = 9;
+ DE_CONNECTION_STATUS = 10;
+ DE_BATTERY_STATUS = 11;
+ // Reserves 128 Test DEs.
+ DE_TEST_BEGIN = 2147483520; // INT_MAX - 127
+ DE_TEST_END = 2147483647; // INT_MAX
+ }
+
+ optional int32 key = 1;
+ optional bytes value = 2;
+ optional uint32 value_length = 3;
+}
+
+// A single filter used to filter BLE events.
message BleFilter {
optional uint32 id = 1; // Required, unique id of this filter.
// Maximum delay to notify the client after an event occurs.
@@ -71,7 +89,9 @@
// the period of latency defined above.
optional float distance_m = 7;
// Used to verify the list of trusted devices.
- repeated PublicateCertificate certficate = 8;
+ repeated PublicateCertificate certificate = 8;
+ // Data Elements for extended properties.
+ repeated DataElement data_element = 9;
}
message BleFilters {
@@ -80,14 +100,33 @@
// FilterResult is returned to host when a BLE event matches a Filter.
message BleFilterResult {
+ enum ResultType {
+ RESULT_NONE = 0;
+ RESULT_PRESENCE = 1;
+ RESULT_FAST_PAIR = 2;
+ }
+
optional uint32 id = 1; // id of the matched Filter.
- optional uint32 tx_power = 2;
- optional uint32 rssi = 3;
+ optional int32 tx_power = 2;
+ optional int32 rssi = 3;
optional uint32 intent = 4;
optional bytes bluetooth_address = 5;
optional PublicCredential public_credential = 6;
+ repeated DataElement data_element = 7;
+ optional bytes ble_service_data = 8;
+ optional ResultType result_type = 9;
}
message BleFilterResults {
repeated BleFilterResult result = 1;
}
+
+message BleConfig {
+ // True to start BLE scan. Otherwise, stop BLE scan.
+ optional bool start_scan = 1;
+ // True when screen is turned on. Otherwise, set to false when screen is
+ // turned off.
+ optional bool screen_on = 2;
+ // Fast Pair cache expires after this time period.
+ optional uint64 fast_pair_cache_expire_time_sec = 3;
+}
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 0410cd5..66a1ffe 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -26,7 +26,7 @@
"bluetooth-test-util-lib",
"compatibility-device-util-axt",
"ctstestrunner-axt",
- "truth-prebuilt",
+ "truth",
],
libs: [
"android.test.base",
@@ -41,7 +41,6 @@
"mts-tethering",
],
certificate: "platform",
- platform_apis: true,
sdk_version: "module_current",
min_sdk_version: "30",
target_sdk_version: "32",
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
index aacb6d8..a2da967 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
@@ -42,7 +42,6 @@
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testBuilder() {
CredentialElement element = new CredentialElement(KEY, VALUE);
-
assertThat(element.getKey()).isEqualTo(KEY);
assertThat(Arrays.equals(element.getValue(), VALUE)).isTrue();
}
@@ -58,9 +57,31 @@
CredentialElement elementFromParcel = element.CREATOR.createFromParcel(
parcel);
parcel.recycle();
-
assertThat(elementFromParcel.getKey()).isEqualTo(KEY);
assertThat(Arrays.equals(elementFromParcel.getValue(), VALUE)).isTrue();
}
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ CredentialElement element = new CredentialElement(KEY, VALUE);
+ assertThat(element.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEqual() {
+ CredentialElement element1 = new CredentialElement(KEY, VALUE);
+ CredentialElement element2 = new CredentialElement(KEY, VALUE);
+ assertThat(element1.equals(element2)).isTrue();
+ assertThat(element1.hashCode()).isEqualTo(element2.hashCode());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreatorNewArray() {
+ CredentialElement [] elements =
+ CredentialElement.CREATOR.newArray(2);
+ assertThat(elements.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
index 3654d0d..84814ae 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
@@ -16,6 +16,13 @@
package android.nearby.cts;
+import static android.nearby.DataElement.DataType.PRIVATE_IDENTITY;
+import static android.nearby.DataElement.DataType.PROVISIONED_IDENTITY;
+import static android.nearby.DataElement.DataType.PUBLIC_IDENTITY;
+import static android.nearby.DataElement.DataType.SALT;
+import static android.nearby.DataElement.DataType.TRUSTED_IDENTITY;
+import static android.nearby.DataElement.DataType.TX_POWER;
+
import static com.google.common.truth.Truth.assertThat;
import android.nearby.DataElement;
@@ -31,7 +38,6 @@
import java.util.Arrays;
-
@RunWith(AndroidJUnit4.class)
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class DataElementTest {
@@ -63,4 +69,59 @@
assertThat(elementFromParcel.getKey()).isEqualTo(KEY);
assertThat(Arrays.equals(elementFromParcel.getValue(), VALUE)).isTrue();
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ DataElement dataElement = new DataElement(KEY, VALUE);
+ assertThat(dataElement.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreatorNewArray() {
+ DataElement[] elements =
+ DataElement.CREATOR.newArray(2);
+ assertThat(elements.length).isEqualTo(2);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEquals() {
+ DataElement dataElement = new DataElement(KEY, VALUE);
+ DataElement dataElement2 = new DataElement(KEY, VALUE);
+
+ assertThat(dataElement.equals(dataElement2)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIsIdentity() {
+ DataElement privateIdentity = new DataElement(PRIVATE_IDENTITY, new byte[]{1, 2, 3});
+ DataElement trustedIdentity = new DataElement(TRUSTED_IDENTITY, new byte[]{1, 2, 3});
+ DataElement publicIdentity = new DataElement(PUBLIC_IDENTITY, new byte[]{1, 2, 3});
+ DataElement provisionedIdentity =
+ new DataElement(PROVISIONED_IDENTITY, new byte[]{1, 2, 3});
+
+ DataElement salt = new DataElement(SALT, new byte[]{1, 2, 3});
+ DataElement txPower = new DataElement(TX_POWER, new byte[]{1, 2, 3});
+
+ assertThat(privateIdentity.isIdentityDataType()).isTrue();
+ assertThat(trustedIdentity.isIdentityDataType()).isTrue();
+ assertThat(publicIdentity.isIdentityDataType()).isTrue();
+ assertThat(provisionedIdentity.isIdentityDataType()).isTrue();
+ assertThat(salt.isIdentityDataType()).isFalse();
+ assertThat(txPower.isIdentityDataType()).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_notEquals() {
+ DataElement dataElement = new DataElement(KEY, VALUE);
+ DataElement dataElement2 = new DataElement(KEY, new byte[]{1, 2, 1, 1});
+ DataElement dataElement3 = new DataElement(6, VALUE);
+
+ assertThat(dataElement.equals(dataElement2)).isFalse();
+ assertThat(dataElement.equals(dataElement3)).isFalse();
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
index f37800a..8ca5a94 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
@@ -16,6 +16,8 @@
package android.nearby.cts;
+import static android.nearby.NearbyDevice.Medium.BLE;
+
import android.annotation.TargetApi;
import android.nearby.FastPairDevice;
import android.nearby.NearbyDevice;
@@ -34,13 +36,18 @@
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@TargetApi(Build.VERSION_CODES.TIRAMISU)
public class NearbyDeviceTest {
+ private static final String NAME = "NearbyDevice";
+ private static final String MODEL_ID = "112233";
+ private static final int TX_POWER = -10;
+ private static final int RSSI = -60;
+ private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
+ private static final byte[] SCAN_DATA = new byte[] {1, 2, 3, 4};
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void test_isValidMedium() {
assertThat(NearbyDevice.isValidMedium(1)).isTrue();
assertThat(NearbyDevice.isValidMedium(2)).isTrue();
-
assertThat(NearbyDevice.isValidMedium(0)).isFalse();
assertThat(NearbyDevice.isValidMedium(3)).isFalse();
}
@@ -49,11 +56,55 @@
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void test_getMedium_fromChild() {
FastPairDevice fastPairDevice = new FastPairDevice.Builder()
- .addMedium(NearbyDevice.Medium.BLE)
- .setRssi(-60)
+ .addMedium(BLE)
+ .setRssi(RSSI)
.build();
assertThat(fastPairDevice.getMediums()).contains(1);
- assertThat(fastPairDevice.getRssi()).isEqualTo(-60);
+ assertThat(fastPairDevice.getRssi()).isEqualTo(RSSI);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEqual() {
+ FastPairDevice fastPairDevice1 = new FastPairDevice.Builder()
+ .setModelId(MODEL_ID)
+ .setTxPower(TX_POWER)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setData(SCAN_DATA)
+ .setRssi(RSSI)
+ .addMedium(BLE)
+ .setName(NAME)
+ .build();
+ FastPairDevice fastPairDevice2 = new FastPairDevice.Builder()
+ .setModelId(MODEL_ID)
+ .setTxPower(TX_POWER)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setData(SCAN_DATA)
+ .setRssi(RSSI)
+ .addMedium(BLE)
+ .setName(NAME)
+ .build();
+
+ assertThat(fastPairDevice1.equals(fastPairDevice1)).isTrue();
+ assertThat(fastPairDevice1.equals(fastPairDevice2)).isTrue();
+ assertThat(fastPairDevice1.equals(null)).isFalse();
+ assertThat(fastPairDevice1.hashCode()).isEqualTo(fastPairDevice2.hashCode());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testToString() {
+ FastPairDevice fastPairDevice1 = new FastPairDevice.Builder()
+ .addMedium(BLE)
+ .setRssi(RSSI)
+ .setModelId(MODEL_ID)
+ .setTxPower(TX_POWER)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .build();
+
+ assertThat(fastPairDevice1.toString())
+ .isEqualTo("FastPairDevice [medium={BLE} rssi=-60 "
+ + "txPower=-10 modelId=112233 bluetoothAddress=00:11:22:33:FF:EE]");
}
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index 7696a61..bc9691d 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -20,7 +20,7 @@
import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+import static android.nearby.ScanCallback.ERROR_UNSUPPORTED;
import static com.google.common.truth.Truth.assertThat;
@@ -35,7 +35,9 @@
import android.nearby.BroadcastRequest;
import android.nearby.NearbyDevice;
import android.nearby.NearbyManager;
+import android.nearby.OffloadCapability;
import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceDevice;
import android.nearby.PrivateCredential;
import android.nearby.ScanCallback;
import android.nearby.ScanRequest;
@@ -48,6 +50,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
+import com.android.modules.utils.build.SdkLevel;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -57,6 +61,7 @@
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
/**
* TODO(b/215435939) This class doesn't include any logic yet. Because SELinux denies access to
@@ -66,7 +71,7 @@
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class NearbyManagerTest {
private static final byte[] SALT = new byte[]{1, 2};
- private static final byte[] SECRETE_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
private static final byte[] META_DATA_ENCRYPTION_KEY = new byte[14];
private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
private static final String DEVICE_NAME = "test_device";
@@ -82,6 +87,9 @@
.setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
.setBleEnabled(true)
.build();
+ private PresenceDevice.Builder mBuilder =
+ new PresenceDevice.Builder("deviceId", SALT, SECRET_ID, META_DATA_ENCRYPTION_KEY);
+
private ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onDiscovered(@NonNull NearbyDevice device) {
@@ -94,14 +102,21 @@
@Override
public void onLost(@NonNull NearbyDevice device) {
}
+
+ @Override
+ public void onError(int errorCode) {
+ }
};
+
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
@Before
public void setUp() {
mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG,
BLUETOOTH_PRIVILEGED);
- DeviceConfig.setProperty(NAMESPACE_TETHERING,
+ String nameSpace = SdkLevel.isAtLeastU() ? DeviceConfig.NAMESPACE_NEARBY
+ : DeviceConfig.NAMESPACE_TETHERING;
+ DeviceConfig.setProperty(nameSpace,
"nearby_enable_presence_broadcast_legacy",
"true", false);
@@ -137,7 +152,7 @@
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testStartStopBroadcast() throws InterruptedException {
- PrivateCredential credential = new PrivateCredential.Builder(SECRETE_ID, AUTHENTICITY_KEY,
+ PrivateCredential credential = new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY,
META_DATA_ENCRYPTION_KEY, DEVICE_NAME)
.setIdentityType(IDENTITY_TYPE_PRIVATE)
.build();
@@ -158,6 +173,22 @@
mNearbyManager.stopBroadcast(callback);
}
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void queryOffloadScanSupport() {
+ OffloadCallback callback = new OffloadCallback();
+ mNearbyManager.queryOffloadCapability(EXECUTOR, callback);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void testAllCallbackMethodsExits() {
+ mScanCallback.onDiscovered(mBuilder.setRssi(-10).build());
+ mScanCallback.onUpdated(mBuilder.setRssi(-5).build());
+ mScanCallback.onLost(mBuilder.setRssi(-8).build());
+ mScanCallback.onError(ERROR_UNSUPPORTED);
+ }
+
private void enableBluetooth() {
BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
BluetoothAdapter bluetoothAdapter = manager.getAdapter();
@@ -165,4 +196,11 @@
assertThat(BTAdapterUtils.enableAdapter(bluetoothAdapter, mContext)).isTrue();
}
}
+
+ private static class OffloadCallback implements Consumer<OffloadCapability> {
+ @Override
+ public void accept(OffloadCapability aBoolean) {
+ // no-op for now
+ }
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/OffloadCapabilityTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/OffloadCapabilityTest.java
new file mode 100644
index 0000000..a745c7d
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/OffloadCapabilityTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package android.nearby.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.OffloadCapability;
+import android.os.Build;
+import android.os.Parcel;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+public class OffloadCapabilityTest {
+ private static final long VERSION = 123456;
+
+ @Test
+ public void testDefault() {
+ OffloadCapability offloadCapability = new OffloadCapability.Builder().build();
+
+ assertThat(offloadCapability.isFastPairSupported()).isFalse();
+ assertThat(offloadCapability.isNearbyShareSupported()).isFalse();
+ assertThat(offloadCapability.getVersion()).isEqualTo(0);
+ }
+
+ @Test
+ public void testBuilder() {
+ OffloadCapability offloadCapability = new OffloadCapability.Builder()
+ .setFastPairSupported(true)
+ .setNearbyShareSupported(true)
+ .setVersion(VERSION)
+ .build();
+
+ assertThat(offloadCapability.isFastPairSupported()).isTrue();
+ assertThat(offloadCapability.isNearbyShareSupported()).isTrue();
+ assertThat(offloadCapability.getVersion()).isEqualTo(VERSION);
+ }
+
+ @Test
+ public void testWriteParcel() {
+ OffloadCapability offloadCapability = new OffloadCapability.Builder()
+ .setFastPairSupported(true)
+ .setNearbyShareSupported(false)
+ .setVersion(VERSION)
+ .build();
+
+ Parcel parcel = Parcel.obtain();
+ offloadCapability.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ OffloadCapability capability = OffloadCapability.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ assertThat(capability.isFastPairSupported()).isTrue();
+ assertThat(capability.isNearbyShareSupported()).isFalse();
+ assertThat(capability.getVersion()).isEqualTo(VERSION);
+ }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
index eaa5ca1..71be889 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
@@ -114,4 +114,18 @@
assertThat(parcelRequest.getType()).isEqualTo(BROADCAST_TYPE_NEARBY_PRESENCE);
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ PresenceBroadcastRequest broadcastRequest = mBuilder.build();
+ assertThat(broadcastRequest.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ PresenceBroadcastRequest[] presenceBroadcastRequests =
+ PresenceBroadcastRequest.CREATOR.newArray(2);
+ assertThat(presenceBroadcastRequests.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
index 94f8fe7..ea1de6b 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
@@ -104,4 +104,24 @@
assertThat(parcelDevice.getMediums()).containsExactly(MEDIUM);
assertThat(parcelDevice.getName()).isEqualTo(DEVICE_NAME);
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ PresenceDevice device =
+ new PresenceDevice.Builder(DEVICE_ID, SALT, SECRET_ID, ENCRYPTED_IDENTITY)
+ .addExtendedProperty(new DataElement(KEY, VALUE))
+ .setRssi(RSSI)
+ .addMedium(MEDIUM)
+ .setName(DEVICE_NAME)
+ .build();
+ assertThat(device.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ PresenceDevice[] devices =
+ PresenceDevice.CREATOR.newArray(2);
+ assertThat(devices.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
index cecdfd2..821f2d0 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
@@ -51,7 +51,6 @@
private static final int KEY = 3;
private static final byte[] VALUE = new byte[]{1, 1, 1, 1};
-
private PublicCredential mPublicCredential =
new PublicCredential.Builder(SECRETE_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
ENCRYPTED_METADATA, METADATA_ENCRYPTION_KEY_TAG)
@@ -90,5 +89,21 @@
assertThat(parcelFilter.getType()).isEqualTo(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE);
assertThat(parcelFilter.getMaxPathLoss()).isEqualTo(RSSI);
assertThat(parcelFilter.getPresenceActions()).containsExactly(ACTION);
+ assertThat(parcelFilter.getExtendedProperties().get(0).getKey()).isEqualTo(KEY);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ PresenceScanFilter filter = mBuilder.build();
+ assertThat(filter.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreatorNewArray() {
+ PresenceScanFilter[] filters =
+ PresenceScanFilter.CREATOR.newArray(2);
+ assertThat(filters.length).isEqualTo(2);
}
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
index f05f65f..fa8c954 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
@@ -99,4 +99,19 @@
assertThat(credentialElement.getKey()).isEqualTo(KEY);
assertThat(Arrays.equals(credentialElement.getValue(), VALUE)).isTrue();
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void describeContents() {
+ PrivateCredential credential = mBuilder.build();
+ assertThat(credential.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testCreatorNewArray() {
+ PrivateCredential[] credentials =
+ PrivateCredential.CREATOR.newArray(2);
+ assertThat(credentials.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
index 11bbacc..774e897 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
@@ -135,6 +135,7 @@
.setIdentityType(IDENTITY_TYPE_PRIVATE)
.build();
assertThat(credentialOne.equals((Object) credentialTwo)).isTrue();
+ assertThat(credentialOne.equals(null)).isFalse();
}
@Test
@@ -161,4 +162,19 @@
.build();
assertThat(credentialOne.equals((Object) credentialTwo)).isFalse();
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ PublicCredential credential = mBuilder.build();
+ assertThat(credential.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreatorNewArray() {
+ PublicCredential[] credentials =
+ PublicCredential.CREATOR.newArray(2);
+ assertThat(credentials.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
index 21f3d28..5ad52c2 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
@@ -30,7 +30,6 @@
import android.nearby.PublicCredential;
import android.nearby.ScanRequest;
import android.os.Build;
-import android.os.WorkSource;
import androidx.annotation.RequiresApi;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -43,12 +42,10 @@
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class ScanRequestTest {
- private static final int UID = 1001;
- private static final String APP_NAME = "android.nearby.tests";
private static final int RSSI = -40;
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testScanType() {
ScanRequest request = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
@@ -59,13 +56,13 @@
// Valid scan type must be set to one of ScanRequest#SCAN_TYPE_
@Test(expected = IllegalStateException.class)
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testScanType_notSet_throwsException() {
new ScanRequest.Builder().setScanMode(SCAN_MODE_BALANCED).build();
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testScanMode_defaultLowPower() {
ScanRequest request = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_FAST_PAIR)
@@ -76,7 +73,7 @@
/** Verify setting work source with null value in the scan request is allowed */
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testSetWorkSource_nullValue() {
ScanRequest request = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_FAST_PAIR)
@@ -87,39 +84,9 @@
assertThat(request.getWorkSource().isEmpty()).isTrue();
}
- /** Verify toString returns expected string. */
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void testToString() {
- WorkSource workSource = getWorkSource();
- ScanRequest request = new ScanRequest.Builder()
- .setScanType(SCAN_TYPE_FAST_PAIR)
- .setScanMode(SCAN_MODE_BALANCED)
- .setBleEnabled(true)
- .setWorkSource(workSource)
- .build();
-
- assertThat(request.toString()).isEqualTo(
- "Request[scanType=1, scanMode=SCAN_MODE_BALANCED, "
- + "enableBle=true, workSource=WorkSource{" + UID + " " + APP_NAME
- + "}, scanFilters=[]]");
- }
-
- /** Verify toString works correctly with null WorkSource. */
- @Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void testToString_nullWorkSource() {
- ScanRequest request = new ScanRequest.Builder().setScanType(
- SCAN_TYPE_FAST_PAIR).setWorkSource(null).build();
-
- assertThat(request.toString()).isEqualTo("Request[scanType=1, "
- + "scanMode=SCAN_MODE_LOW_POWER, enableBle=true, workSource=WorkSource{}, "
- + "scanFilters=[]]");
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void testisEnableBle_defaultTrue() {
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testIsEnableBle_defaultTrue() {
ScanRequest request = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_FAST_PAIR)
.build();
@@ -128,7 +95,28 @@
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void testIsOffloadOnly_defaultFalse() {
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_FAST_PAIR)
+ .build();
+
+ assertThat(request.isOffloadOnly()).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void testSetOffloadOnly_isOffloadOnlyTrue() {
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .setOffloadOnly(true)
+ .build();
+
+ assertThat(request.isOffloadOnly()).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void test_isValidScanType() {
assertThat(ScanRequest.isValidScanType(SCAN_TYPE_FAST_PAIR)).isTrue();
assertThat(ScanRequest.isValidScanType(SCAN_TYPE_NEARBY_PRESENCE)).isTrue();
@@ -138,7 +126,7 @@
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void test_isValidScanMode() {
assertThat(ScanRequest.isValidScanMode(SCAN_MODE_LOW_LATENCY)).isTrue();
assertThat(ScanRequest.isValidScanMode(SCAN_MODE_BALANCED)).isTrue();
@@ -150,7 +138,7 @@
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void test_scanModeToString() {
assertThat(ScanRequest.scanModeToString(2)).isEqualTo("SCAN_MODE_LOW_LATENCY");
assertThat(ScanRequest.scanModeToString(1)).isEqualTo("SCAN_MODE_BALANCED");
@@ -162,7 +150,7 @@
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testScanFilter() {
ScanRequest request = new ScanRequest.Builder().setScanType(
SCAN_TYPE_NEARBY_PRESENCE).addScanFilter(getPresenceScanFilter()).build();
@@ -171,6 +159,23 @@
assertThat(request.getScanFilters().get(0).getMaxPathLoss()).isEqualTo(RSSI);
}
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void describeContents() {
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_FAST_PAIR)
+ .build();
+ assertThat(request.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testCreatorNewArray() {
+ ScanRequest[] requests =
+ ScanRequest.CREATOR.newArray(2);
+ assertThat(requests.length).isEqualTo(2);
+ }
+
private static PresenceScanFilter getPresenceScanFilter() {
final byte[] secretId = new byte[]{1, 2, 3, 4};
final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
@@ -190,8 +195,4 @@
.addPresenceAction(action)
.build();
}
-
- private static WorkSource getWorkSource() {
- return new WorkSource(UID, APP_NAME);
- }
}
diff --git a/nearby/tests/integration/privileged/Android.bp b/nearby/tests/integration/privileged/Android.bp
index e3250f6..9b6e488 100644
--- a/nearby/tests/integration/privileged/Android.bp
+++ b/nearby/tests/integration/privileged/Android.bp
@@ -27,7 +27,7 @@
"androidx.test.ext.junit",
"androidx.test.rules",
"junit",
- "truth-prebuilt",
+ "truth",
],
test_suites: ["device-tests"],
}
diff --git a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
index 66bab23..506b4e2 100644
--- a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
+++ b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
@@ -63,6 +63,8 @@
override fun onUpdated(device: NearbyDevice) {}
override fun onLost(device: NearbyDevice) {}
+
+ override fun onError(errorCode: Int) {}
}
nearbyManager.startScan(scanRequest, /* executor */ { it.run() }, scanCallback)
diff --git a/nearby/tests/integration/untrusted/Android.bp b/nearby/tests/integration/untrusted/Android.bp
index 57499e4..75f765b 100644
--- a/nearby/tests/integration/untrusted/Android.bp
+++ b/nearby/tests/integration/untrusted/Android.bp
@@ -31,7 +31,7 @@
"androidx.test.uiautomator_uiautomator",
"junit",
"kotlin-test",
- "truth-prebuilt",
+ "truth",
],
test_suites: ["device-tests"],
}
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index 9b35452..112c751 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -42,7 +42,7 @@
"mockito-target-extended-minus-junit4",
"platform-test-annotations",
"service-nearby-pre-jarjar",
- "truth-prebuilt",
+ "truth",
// "Robolectric_all-target",
],
// these are needed for Extended Mockito
diff --git a/nearby/tests/unit/AndroidManifest.xml b/nearby/tests/unit/AndroidManifest.xml
index 9f58baf..7dcb263 100644
--- a/nearby/tests/unit/AndroidManifest.xml
+++ b/nearby/tests/unit/AndroidManifest.xml
@@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
diff --git a/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java b/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java
new file mode 100644
index 0000000..edda3c2
--- /dev/null
+++ b/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package android.nearby;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FastPairDeviceTest {
+ private static final String NAME = "name";
+ private static final byte[] DATA = new byte[] {0x01, 0x02};
+ private static final String MODEL_ID = "112233";
+ private static final int RSSI = -80;
+ private static final int TX_POWER = -10;
+ private static final String MAC_ADDRESS = "00:11:22:33:44:55";
+ private static List<Integer> sMediums = new ArrayList<Integer>(List.of(1));
+ private static FastPairDevice sDevice;
+
+
+ @Before
+ public void setup() {
+ sDevice = new FastPairDevice(NAME, sMediums, RSSI, TX_POWER, MODEL_ID, MAC_ADDRESS, DATA);
+ }
+
+ @Test
+ public void testParcelable() {
+ Parcel dest = Parcel.obtain();
+ sDevice.writeToParcel(dest, 0);
+ dest.setDataPosition(0);
+ FastPairDevice compareDevice = FastPairDevice.CREATOR.createFromParcel(dest);
+ assertThat(compareDevice.getName()).isEqualTo(NAME);
+ assertThat(compareDevice.getMediums()).isEqualTo(sMediums);
+ assertThat(compareDevice.getRssi()).isEqualTo(RSSI);
+ assertThat(compareDevice.getTxPower()).isEqualTo(TX_POWER);
+ assertThat(compareDevice.getModelId()).isEqualTo(MODEL_ID);
+ assertThat(compareDevice.getBluetoothAddress()).isEqualTo(MAC_ADDRESS);
+ assertThat(compareDevice.getData()).isEqualTo(DATA);
+ assertThat(compareDevice.equals(sDevice)).isTrue();
+ assertThat(compareDevice.hashCode()).isEqualTo(sDevice.hashCode());
+ }
+
+ @Test
+ public void describeContents() {
+ assertThat(sDevice.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void testToString() {
+ assertThat(sDevice.toString()).isEqualTo(
+ "FastPairDevice [name=name, medium={BLE} "
+ + "rssi=-80 txPower=-10 "
+ + "modelId=112233 bluetoothAddress=00:11:22:33:44:55]");
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ FastPairDevice[] fastPairDevices = FastPairDevice.CREATOR.newArray(2);
+ assertThat(fastPairDevices.length).isEqualTo(2);
+ }
+
+ @Test
+ public void testBuilder() {
+ FastPairDevice.Builder builder = new FastPairDevice.Builder();
+ FastPairDevice compareDevice = builder.setName(NAME)
+ .addMedium(1)
+ .setBluetoothAddress(MAC_ADDRESS)
+ .setRssi(RSSI)
+ .setTxPower(TX_POWER)
+ .setData(DATA)
+ .setModelId(MODEL_ID)
+ .build();
+ assertThat(compareDevice.getName()).isEqualTo(NAME);
+ assertThat(compareDevice.getMediums()).isEqualTo(sMediums);
+ assertThat(compareDevice.getRssi()).isEqualTo(RSSI);
+ assertThat(compareDevice.getTxPower()).isEqualTo(TX_POWER);
+ assertThat(compareDevice.getModelId()).isEqualTo(MODEL_ID);
+ assertThat(compareDevice.getBluetoothAddress()).isEqualTo(MAC_ADDRESS);
+ assertThat(compareDevice.getData()).isEqualTo(DATA);
+ }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java b/nearby/tests/unit/src/android/nearby/NearbyDeviceParcelableTest.java
similarity index 60%
rename from nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
rename to nearby/tests/unit/src/android/nearby/NearbyDeviceParcelableTest.java
index 654b852..a4909b2 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
+++ b/nearby/tests/unit/src/android/nearby/NearbyDeviceParcelableTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,20 +14,17 @@
* limitations under the License.
*/
-package android.nearby.cts;
+package android.nearby;
import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
import static com.google.common.truth.Truth.assertThat;
-import android.nearby.NearbyDevice;
-import android.nearby.NearbyDeviceParcelable;
import android.os.Build;
import android.os.Parcel;
import androidx.annotation.RequiresApi;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
import org.junit.Before;
import org.junit.Test;
@@ -39,10 +36,15 @@
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class NearbyDeviceParcelableTest {
+ private static final long DEVICE_ID = 1234;
private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
private static final byte[] SCAN_DATA = new byte[] {1, 2, 3, 4};
+ private static final byte[] SALT = new byte[] {1, 2, 3, 4};
private static final String FAST_PAIR_MODEL_ID = "1234";
private static final int RSSI = -60;
+ private static final int TX_POWER = -10;
+ private static final int ACTION = 1;
+ private static final int MEDIUM_BLE = 1;
private NearbyDeviceParcelable.Builder mBuilder;
@@ -50,9 +52,10 @@
public void setUp() {
mBuilder =
new NearbyDeviceParcelable.Builder()
+ .setDeviceId(DEVICE_ID)
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.setName("testDevice")
- .setMedium(NearbyDevice.Medium.BLE)
+ .setMedium(MEDIUM_BLE)
.setRssi(RSSI)
.setFastPairModelId(FAST_PAIR_MODEL_ID)
.setBluetoothAddress(BLUETOOTH_ADDRESS)
@@ -60,25 +63,40 @@
}
@Test
- @SdkSuppress(minSdkVersion = 33, codeName = "T")
- public void test_defaultNullFields() {
+ public void testNullFields() {
+ PublicCredential publicCredential =
+ new PublicCredential.Builder(
+ new byte[] {1},
+ new byte[] {2},
+ new byte[] {3},
+ new byte[] {4},
+ new byte[] {5})
+ .build();
NearbyDeviceParcelable nearbyDeviceParcelable =
new NearbyDeviceParcelable.Builder()
- .setMedium(NearbyDevice.Medium.BLE)
+ .setMedium(MEDIUM_BLE)
+ .setPublicCredential(publicCredential)
+ .setAction(ACTION)
.setRssi(RSSI)
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .setTxPower(TX_POWER)
+ .setSalt(SALT)
.build();
+ assertThat(nearbyDeviceParcelable.getDeviceId()).isEqualTo(-1);
assertThat(nearbyDeviceParcelable.getName()).isNull();
assertThat(nearbyDeviceParcelable.getFastPairModelId()).isNull();
assertThat(nearbyDeviceParcelable.getBluetoothAddress()).isNull();
assertThat(nearbyDeviceParcelable.getData()).isNull();
-
- assertThat(nearbyDeviceParcelable.getMedium()).isEqualTo(NearbyDevice.Medium.BLE);
+ assertThat(nearbyDeviceParcelable.getMedium()).isEqualTo(MEDIUM_BLE);
assertThat(nearbyDeviceParcelable.getRssi()).isEqualTo(RSSI);
+ assertThat(nearbyDeviceParcelable.getAction()).isEqualTo(ACTION);
+ assertThat(nearbyDeviceParcelable.getPublicCredential()).isEqualTo(publicCredential);
+ assertThat(nearbyDeviceParcelable.getSalt()).isEqualTo(SALT);
+ assertThat(nearbyDeviceParcelable.getTxPower()).isEqualTo(TX_POWER);
}
@Test
- @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testWriteParcel() {
NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.build();
@@ -89,6 +107,7 @@
NearbyDeviceParcelable.CREATOR.createFromParcel(parcel);
parcel.recycle();
+ assertThat(actualNearbyDevice.getDeviceId()).isEqualTo(DEVICE_ID);
assertThat(actualNearbyDevice.getRssi()).isEqualTo(RSSI);
assertThat(actualNearbyDevice.getFastPairModelId()).isEqualTo(FAST_PAIR_MODEL_ID);
assertThat(actualNearbyDevice.getBluetoothAddress()).isEqualTo(BLUETOOTH_ADDRESS);
@@ -96,7 +115,6 @@
}
@Test
- @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testWriteParcel_nullModelId() {
NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setFastPairModelId(null).build();
@@ -111,10 +129,8 @@
}
@Test
- @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testWriteParcel_nullBluetoothAddress() {
NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setBluetoothAddress(null).build();
-
Parcel parcel = Parcel.obtain();
nearbyDeviceParcelable.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
@@ -124,4 +140,34 @@
assertThat(actualNearbyDevice.getBluetoothAddress()).isNull();
}
+
+ @Test
+ public void describeContents() {
+ NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setBluetoothAddress(null).build();
+ assertThat(nearbyDeviceParcelable.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void testEqual() {
+ PublicCredential publicCredential =
+ new PublicCredential.Builder(
+ new byte[] {1},
+ new byte[] {2},
+ new byte[] {3},
+ new byte[] {4},
+ new byte[] {5})
+ .build();
+ NearbyDeviceParcelable nearbyDeviceParcelable1 =
+ mBuilder.setPublicCredential(publicCredential).build();
+ NearbyDeviceParcelable nearbyDeviceParcelable2 =
+ mBuilder.setPublicCredential(publicCredential).build();
+ assertThat(nearbyDeviceParcelable1.equals(nearbyDeviceParcelable2)).isTrue();
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ NearbyDeviceParcelable[] nearbyDeviceParcelables =
+ NearbyDeviceParcelable.CREATOR.newArray(2);
+ assertThat(nearbyDeviceParcelables.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/unit/src/android/nearby/ScanRequestTest.java b/nearby/tests/unit/src/android/nearby/ScanRequestTest.java
index 12de30e..6020c7e 100644
--- a/nearby/tests/unit/src/android/nearby/ScanRequestTest.java
+++ b/nearby/tests/unit/src/android/nearby/ScanRequestTest.java
@@ -24,11 +24,14 @@
import static com.google.common.truth.Truth.assertThat;
+import android.os.Build;
import android.os.Parcel;
import android.os.WorkSource;
import android.platform.test.annotations.Presubmit;
+import androidx.annotation.RequiresApi;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import org.junit.Test;
@@ -38,14 +41,15 @@
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class ScanRequestTest {
private static final int RSSI = -40;
+ private static final int UID = 1001;
+ private static final String APP_NAME = "android.nearby.tests";
private static WorkSource getWorkSource() {
- final int uid = 1001;
- final String appName = "android.nearby.tests";
- return new WorkSource(uid, appName);
+ return new WorkSource(UID, APP_NAME);
}
/** Test creating a scan request. */
@@ -104,6 +108,7 @@
/** Verify toString returns expected string. */
@Test
+ @SdkSuppress(minSdkVersion = 34)
public void testToString() {
WorkSource workSource = getWorkSource();
ScanRequest request = new ScanRequest.Builder()
@@ -115,28 +120,28 @@
assertThat(request.toString()).isEqualTo(
"Request[scanType=1, scanMode=SCAN_MODE_BALANCED, "
- + "enableBle=true, workSource=WorkSource{1001 android.nearby.tests}, "
- + "scanFilters=[]]");
+ + "bleEnabled=true, offloadOnly=false, "
+ + "workSource=WorkSource{" + UID + " " + APP_NAME + "}, scanFilters=[]]");
}
/** Verify toString works correctly with null WorkSource. */
@Test
- public void testToString_nullWorkSource() {
+ @SdkSuppress(minSdkVersion = 34)
+ public void testToString_nullWorkSource_offloadOnly() {
ScanRequest request = new ScanRequest.Builder().setScanType(
- SCAN_TYPE_FAST_PAIR).setWorkSource(null).build();
+ SCAN_TYPE_FAST_PAIR).setWorkSource(null).setOffloadOnly(true).build();
assertThat(request.toString()).isEqualTo("Request[scanType=1, "
- + "scanMode=SCAN_MODE_LOW_POWER, enableBle=true, workSource=WorkSource{}, "
- + "scanFilters=[]]");
+ + "scanMode=SCAN_MODE_LOW_POWER, bleEnabled=true, offloadOnly=true, "
+ + "workSource=WorkSource{}, scanFilters=[]]");
}
/** Verify writing and reading from parcel for scan request. */
@Test
public void testParceling() {
- final int scanType = SCAN_TYPE_NEARBY_PRESENCE;
WorkSource workSource = getWorkSource();
ScanRequest originalRequest = new ScanRequest.Builder()
- .setScanType(scanType)
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.setScanMode(SCAN_MODE_BALANCED)
.setBleEnabled(true)
.setWorkSource(workSource)
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
new file mode 100644
index 0000000..5ddfed3
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby;
+
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY;
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_MAINLINE_NANO_APP_MIN_VERSION;
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.provider.DeviceConfig;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public final class NearbyConfigurationTest {
+
+ private static final String NAMESPACE = NearbyConfiguration.getNamespace();
+ private NearbyConfiguration mNearbyConfiguration;
+
+ @Before
+ public void setUp() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+ }
+
+ @Test
+ public void testDeviceConfigChanged() throws InterruptedException {
+ mNearbyConfiguration = new NearbyConfiguration();
+
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+ "false", false);
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+ "false", false);
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+ "1", false);
+ Thread.sleep(500);
+
+ assertThat(mNearbyConfiguration.isTestAppSupported()).isFalse();
+ assertThat(mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()).isFalse();
+ assertThat(mNearbyConfiguration.getNanoAppMinVersion()).isEqualTo(1);
+
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+ "true", false);
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+ "true", false);
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+ "3", false);
+ Thread.sleep(500);
+
+ assertThat(mNearbyConfiguration.isTestAppSupported()).isTrue();
+ assertThat(mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()).isTrue();
+ assertThat(mNearbyConfiguration.getNanoAppMinVersion()).isEqualTo(3);
+ }
+
+ @After
+ public void tearDown() {
+ // Sets DeviceConfig values back to default
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+ "false", true);
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+ "false", true);
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+ "0", true);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
index 8a18cca..5b640cc 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
@@ -18,6 +18,9 @@
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -32,6 +35,8 @@
import android.content.Context;
import android.nearby.IScanListener;
import android.nearby.ScanRequest;
+import android.os.IBinder;
+import android.provider.DeviceConfig;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -45,6 +50,7 @@
public final class NearbyServiceTest {
+ private static final String NAMESPACE = NearbyConfiguration.getNamespace();
private static final String PACKAGE_NAME = "android.nearby.test";
private Context mContext;
private NearbyService mService;
@@ -56,11 +62,16 @@
private IScanListener mScanListener;
@Mock
private AppOpsManager mMockAppOpsManager;
+ @Mock
+ private IBinder mIBinder;
@Before
public void setUp() {
initMocks(this);
- mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, BLUETOOTH_PRIVILEGED);
+ when(mScanListener.asBinder()).thenReturn(mIBinder);
+
+ mUiAutomation.adoptShellPermissionIdentity(
+ READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG, BLUETOOTH_PRIVILEGED);
mContext = InstrumentationRegistry.getInstrumentation().getContext();
mService = new NearbyService(mContext);
mScanRequest = createScanRequest();
@@ -80,6 +91,8 @@
@Test
public void test_register_noPrivilegedPermission_throwsException() {
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+ "false", false);
mUiAutomation.dropShellPermissionIdentity();
assertThrows(java.lang.SecurityException.class,
() -> mService.registerScanListener(mScanRequest, mScanListener, PACKAGE_NAME,
@@ -88,6 +101,8 @@
@Test
public void test_unregister_noPrivilegedPermission_throwsException() {
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+ "false", false);
mUiAutomation.dropShellPermissionIdentity();
assertThrows(java.lang.SecurityException.class,
() -> mService.unregisterScanListener(mScanListener, PACKAGE_NAME,
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java
new file mode 100644
index 0000000..719e816
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.SystemClock;
+
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class CancelableAlarmTest {
+
+ private static final long DELAY_MILLIS = 1000;
+
+ private final ScheduledExecutorService mExecutor =
+ Executors.newScheduledThreadPool(1);
+
+ @Test
+ public void alarmRuns_singleExecution() throws InterruptedException {
+ TestCountDownLatch latch = new TestCountDownLatch(1);
+ CancelableAlarm.createSingleAlarm(
+ "alarmRuns", new CountDownRunnable(latch), DELAY_MILLIS, mExecutor);
+ latch.awaitAndExpectDelay(DELAY_MILLIS);
+ }
+
+ @Test
+ public void alarmRuns_periodicExecution() throws InterruptedException {
+ TestCountDownLatch latch = new TestCountDownLatch(2);
+ CancelableAlarm.createRecurringAlarm(
+ "alarmRunsPeriodically", new CountDownRunnable(latch), DELAY_MILLIS, mExecutor);
+ latch.awaitAndExpectDelay(DELAY_MILLIS * 2);
+ }
+
+ @Test
+ public void canceledAlarmDoesNotRun_singleExecution() throws InterruptedException {
+ TestCountDownLatch latch = new TestCountDownLatch(1);
+ CancelableAlarm alarm =
+ CancelableAlarm.createSingleAlarm(
+ "canceledAlarmDoesNotRun",
+ new CountDownRunnable(latch),
+ DELAY_MILLIS,
+ mExecutor);
+ assertThat(alarm.cancel()).isTrue();
+ latch.awaitAndExpectTimeout(DELAY_MILLIS);
+ }
+
+ @Test
+ public void canceledAlarmDoesNotRun_periodicExecution() throws InterruptedException {
+ TestCountDownLatch latch = new TestCountDownLatch(2);
+ CancelableAlarm alarm =
+ CancelableAlarm.createRecurringAlarm(
+ "canceledAlarmDoesNotRunPeriodically",
+ new CountDownRunnable(latch),
+ DELAY_MILLIS,
+ mExecutor);
+ latch.awaitAndExpectTimeout(DELAY_MILLIS);
+ assertThat(alarm.cancel()).isTrue();
+ latch.awaitAndExpectTimeout(DELAY_MILLIS);
+ }
+
+ @Test
+ public void cancelOfRunAlarmReturnsFalse() throws InterruptedException {
+ TestCountDownLatch latch = new TestCountDownLatch(1);
+ long delayMillis = 500;
+ CancelableAlarm alarm =
+ CancelableAlarm.createSingleAlarm(
+ "cancelOfRunAlarmReturnsFalse",
+ new CountDownRunnable(latch),
+ delayMillis,
+ mExecutor);
+ latch.awaitAndExpectDelay(delayMillis - 1);
+
+ assertThat(alarm.cancel()).isFalse();
+ }
+
+ private static class CountDownRunnable implements Runnable {
+ private final CountDownLatch mLatch;
+
+ CountDownRunnable(CountDownLatch latch) {
+ this.mLatch = latch;
+ }
+
+ @Override
+ public void run() {
+ mLatch.countDown();
+ }
+ }
+
+ /** A CountDownLatch for test with extra test features like throw exception on await(). */
+ private static class TestCountDownLatch extends CountDownLatch {
+
+ TestCountDownLatch(int count) {
+ super(count);
+ }
+
+ /**
+ * Asserts that the latch does not go off until delayMillis has passed and that it does in
+ * fact go off after delayMillis has passed.
+ */
+ public void awaitAndExpectDelay(long delayMillis) throws InterruptedException {
+ SystemClock.sleep(delayMillis - 1);
+ assertThat(await(0, TimeUnit.MILLISECONDS)).isFalse();
+ SystemClock.sleep(10);
+ assertThat(await(0, TimeUnit.MILLISECONDS)).isTrue();
+ }
+
+ /** Asserts that the latch does not go off within delayMillis. */
+ public void awaitAndExpectTimeout(long delayMillis) throws InterruptedException {
+ SystemClock.sleep(delayMillis + 1);
+ assertThat(await(0, TimeUnit.MILLISECONDS)).isFalse();
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/CancellationFlagTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/CancellationFlagTest.java
new file mode 100644
index 0000000..eb6316e
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/CancellationFlagTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class CancellationFlagTest {
+
+ @Test
+ public void initialValueIsFalse() {
+ assertThat(new CancellationFlag().isCancelled()).isFalse();
+ }
+
+ @Test
+ public void cancel() {
+ CancellationFlag flag = new CancellationFlag();
+ flag.cancel();
+ assertThat(flag.isCancelled()).isTrue();
+ }
+
+ @Test
+ public void cancelShouldOnlyCancelOnce() {
+ CancellationFlag flag = new CancellationFlag();
+ AtomicInteger record = new AtomicInteger();
+
+ flag.registerOnCancelListener(() -> record.incrementAndGet());
+ for (int i = 0; i < 3; i++) {
+ flag.cancel();
+ }
+
+ assertThat(flag.isCancelled()).isTrue();
+ assertThat(record.get()).isEqualTo(1);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/injector/ContextHubManagerAdapterTest.java b/nearby/tests/unit/src/com/android/server/nearby/injector/ContextHubManagerAdapterTest.java
new file mode 100644
index 0000000..b577064
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/injector/ContextHubManagerAdapterTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.injector;
+
+import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class ContextHubManagerAdapterTest {
+ private ContextHubManagerAdapter mContextHubManagerAdapter;
+
+ @Mock
+ ContextHubManager mContextHubManager;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mContextHubManagerAdapter = new ContextHubManagerAdapter(mContextHubManager);
+ }
+
+ @Test
+ public void getContextHubs() {
+ mContextHubManagerAdapter.getContextHubs();
+ }
+
+ @Test
+ public void queryNanoApps() {
+ mContextHubManagerAdapter.queryNanoApps(new ContextHubInfo());
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
similarity index 79%
rename from nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
rename to nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
index d45d570..bc38210 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-package com.android.server.nearby.provider;
+package com.android.server.nearby.managers;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.server.nearby.NearbyConfiguration.NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY;
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.eq;
@@ -39,6 +39,9 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.server.nearby.NearbyConfiguration;
+import com.android.server.nearby.provider.BleBroadcastProvider;
+
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.Before;
@@ -51,9 +54,10 @@
import java.util.Collections;
/**
- * Unit test for {@link BroadcastProviderManager}.
+ * Unit test for {@link com.android.server.nearby.managers.BroadcastProviderManager}.
*/
public class BroadcastProviderManagerTest {
+ private static final String NAMESPACE = NearbyConfiguration.getNamespace();
private static final byte[] IDENTITY = new byte[]{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
private static final int MEDIUM_TYPE_BLE = 0;
private static final byte[] SALT = {2, 3};
@@ -79,11 +83,12 @@
@Before
public void setUp() {
mUiAutomation.adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
- DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
- "true", false);
+ DeviceConfig.setProperty(
+ NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, "true", false);
mContext = ApplicationProvider.getApplicationContext();
- mBroadcastProviderManager = new BroadcastProviderManager(MoreExecutors.directExecutor(),
+ mBroadcastProviderManager = new BroadcastProviderManager(
+ MoreExecutors.directExecutor(),
mBleBroadcastProvider);
PrivateCredential privateCredential =
@@ -101,14 +106,22 @@
@Test
public void testStartAdvertising() {
mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
- verify(mBleBroadcastProvider).start(any(byte[].class), any(
- BleBroadcastProvider.BroadcastListener.class));
+ verify(mBleBroadcastProvider).start(eq(BroadcastRequest.PRESENCE_VERSION_V0),
+ any(byte[].class), any(BleBroadcastProvider.BroadcastListener.class));
+ }
+
+ @Test
+ public void testStopAdvertising() {
+ mBroadcastProviderManager.stopBroadcast(mBroadcastListener);
}
@Test
public void testStartAdvertising_featureDisabled() throws Exception {
- DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
- "false", false);
+ DeviceConfig.setProperty(
+ NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, "false", false);
+ DeviceConfig.setProperty(
+ NAMESPACE, NEARBY_SUPPORT_TEST_APP, "false", false);
+
mBroadcastProviderManager = new BroadcastProviderManager(MoreExecutors.directExecutor(),
mBleBroadcastProvider);
mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
new file mode 100644
index 0000000..aa0dad3
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanRequest;
+import android.os.IBinder;
+
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.provider.DiscoveryProviderController;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Unit test for {@link DiscoveryProviderManagerLegacy} class.
+ */
+public class DiscoveryProviderManagerLegacyTest {
+ private static final int SCAN_MODE_CHRE_ONLY = 3;
+ private static final int DATA_TYPE_SCAN_MODE = 102;
+ private static final int UID = 1234;
+ private static final int PID = 5678;
+ private static final String PACKAGE_NAME = "android.nearby.test";
+ private static final int RSSI = -60;
+ @Mock
+ Injector mInjector;
+ @Mock
+ Context mContext;
+ @Mock
+ AppOpsManager mAppOpsManager;
+ @Mock
+ BleDiscoveryProvider mBleDiscoveryProvider;
+ @Mock
+ ChreDiscoveryProvider mChreDiscoveryProvider;
+ @Mock
+ DiscoveryProviderController mBluetoothController;
+ @Mock
+ DiscoveryProviderController mChreController;
+ @Mock
+ IScanListener mScanListener;
+ @Mock
+ CallerIdentity mCallerIdentity;
+ @Mock
+ DiscoveryProviderManagerLegacy.ScanListenerDeathRecipient mScanListenerDeathRecipient;
+ @Mock
+ IBinder mIBinder;
+ private DiscoveryProviderManagerLegacy mDiscoveryProviderManager;
+ private Map<IBinder, DiscoveryProviderManagerLegacy.ScanListenerRecord>
+ mScanTypeScanListenerRecordMap;
+
+ private static PresenceScanFilter getPresenceScanFilter() {
+ final byte[] secretId = new byte[]{1, 2, 3, 4};
+ final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+ final byte[] publicKey = new byte[]{1, 1, 2, 2};
+ final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+ final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+ PublicCredential credential = new PublicCredential.Builder(
+ secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+
+ final int action = 123;
+ return new PresenceScanFilter.Builder()
+ .addCredential(credential)
+ .setMaxPathLoss(RSSI)
+ .addPresenceAction(action)
+ .build();
+ }
+
+ private static PresenceScanFilter getChreOnlyPresenceScanFilter() {
+ final byte[] secretId = new byte[]{1, 2, 3, 4};
+ final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+ final byte[] publicKey = new byte[]{1, 1, 2, 2};
+ final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+ final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+ PublicCredential credential = new PublicCredential.Builder(
+ secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+
+ final int action = 123;
+ DataElement scanModeElement = new DataElement(DATA_TYPE_SCAN_MODE,
+ new byte[]{SCAN_MODE_CHRE_ONLY});
+ return new PresenceScanFilter.Builder()
+ .addCredential(credential)
+ .setMaxPathLoss(RSSI)
+ .addPresenceAction(action)
+ .addExtendedProperty(scanModeElement)
+ .build();
+ }
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+ when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
+ when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+
+ mScanTypeScanListenerRecordMap = new HashMap<>();
+ mDiscoveryProviderManager =
+ new DiscoveryProviderManagerLegacy(mContext, mInjector,
+ mBleDiscoveryProvider,
+ mChreDiscoveryProvider,
+ mScanTypeScanListenerRecordMap);
+ mCallerIdentity = CallerIdentity
+ .forTest(UID, PID, PACKAGE_NAME, /* attributionTag= */ null);
+ }
+
+ @Test
+ public void testOnNearbyDeviceDiscovered() {
+ NearbyDeviceParcelable nearbyDeviceParcelable = new NearbyDeviceParcelable.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .build();
+ mDiscoveryProviderManager.onNearbyDeviceDiscovered(nearbyDeviceParcelable);
+ }
+
+ @Test
+ public void testInvalidateProviderScanMode() {
+ mDiscoveryProviderManager.invalidateProviderScanMode();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreAvailable_multipleFilters_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter())
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreUnavailable_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isFalse();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreAvailable_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreUnavailable_bleProviderStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, atLeastOnce()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreUndetermined_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(null);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isNull();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreUndetermined_bleProviderStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(null);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, atLeastOnce()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void test_stopChreProvider_clearFilters() throws Exception {
+ // Cannot use mocked ChreDiscoveryProvider,
+ // so we cannot use class variable mDiscoveryProviderManager
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ DiscoveryProviderManagerLegacy manager =
+ new DiscoveryProviderManagerLegacy(mContext, mInjector,
+ mBleDiscoveryProvider,
+ new ChreDiscoveryProvider(
+ mContext,
+ new ChreCommunication(mInjector, mContext, executor), executor),
+ mScanTypeScanListenerRecordMap);
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ // This is an asynchronized process. The filters will be set in executor thread. So we need
+ // to wait for some time to get the correct result.
+ Thread.sleep(200);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ manager.stopChreProvider();
+ Thread.sleep(200);
+ // The filters should be cleared right after.
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isFalse();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isEmpty();
+ }
+
+ @Test
+ public void test_restartChreProvider() throws Exception {
+ // Cannot use mocked ChreDiscoveryProvider,
+ // so we cannot use class variable mDiscoveryProviderManager
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ DiscoveryProviderManagerLegacy manager =
+ new DiscoveryProviderManagerLegacy(mContext, mInjector,
+ mBleDiscoveryProvider,
+ new ChreDiscoveryProvider(
+ mContext,
+ new ChreCommunication(mInjector, mContext, executor), executor),
+ mScanTypeScanListenerRecordMap);
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ // This is an asynchronized process. The filters will be set in executor thread. So we need
+ // to wait for some time to get the correct result.
+ Thread.sleep(200);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ // We want to make sure quickly restart the provider the filters should
+ // be reset correctly.
+ // See b/255922206, there can be a race condition that filters get cleared because onStop()
+ // get executed after onStart() if they are called from different threads.
+ manager.stopChreProvider();
+ manager.mChreDiscoveryProvider.getController().setProviderScanFilters(
+ List.of(getPresenceScanFilter()));
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ Thread.sleep(200);
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ // Wait for enough time
+ Thread.sleep(1000);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
new file mode 100644
index 0000000..7ecf631
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanRequest;
+import android.os.IBinder;
+
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.provider.DiscoveryProviderController;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class DiscoveryProviderManagerTest {
+ private static final int SCAN_MODE_CHRE_ONLY = 3;
+ private static final int DATA_TYPE_SCAN_MODE = 102;
+ private static final int UID = 1234;
+ private static final int PID = 5678;
+ private static final String PACKAGE_NAME = "android.nearby.test";
+ private static final int RSSI = -60;
+ @Mock
+ Injector mInjector;
+ @Mock
+ Context mContext;
+ @Mock
+ AppOpsManager mAppOpsManager;
+ @Mock
+ BleDiscoveryProvider mBleDiscoveryProvider;
+ @Mock
+ ChreDiscoveryProvider mChreDiscoveryProvider;
+ @Mock
+ DiscoveryProviderController mBluetoothController;
+ @Mock
+ DiscoveryProviderController mChreController;
+ @Mock
+ IScanListener mScanListener;
+ @Mock
+ CallerIdentity mCallerIdentity;
+ @Mock
+ IBinder mIBinder;
+ private Executor mExecutor;
+ private DiscoveryProviderManager mDiscoveryProviderManager;
+
+ private static PresenceScanFilter getPresenceScanFilter() {
+ final byte[] secretId = new byte[]{1, 2, 3, 4};
+ final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+ final byte[] publicKey = new byte[]{1, 1, 2, 2};
+ final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+ final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+ PublicCredential credential = new PublicCredential.Builder(
+ secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+
+ final int action = 123;
+ return new PresenceScanFilter.Builder()
+ .addCredential(credential)
+ .setMaxPathLoss(RSSI)
+ .addPresenceAction(action)
+ .build();
+ }
+
+ private static PresenceScanFilter getChreOnlyPresenceScanFilter() {
+ final byte[] secretId = new byte[]{1, 2, 3, 4};
+ final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+ final byte[] publicKey = new byte[]{1, 1, 2, 2};
+ final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+ final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+ PublicCredential credential = new PublicCredential.Builder(
+ secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+
+ final int action = 123;
+ DataElement scanModeElement = new DataElement(DATA_TYPE_SCAN_MODE,
+ new byte[]{SCAN_MODE_CHRE_ONLY});
+ return new PresenceScanFilter.Builder()
+ .addCredential(credential)
+ .setMaxPathLoss(RSSI)
+ .addPresenceAction(action)
+ .addExtendedProperty(scanModeElement)
+ .build();
+ }
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mExecutor = Executors.newSingleThreadExecutor();
+ when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+ when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
+ when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+ when(mScanListener.asBinder()).thenReturn(mIBinder);
+
+ mDiscoveryProviderManager =
+ new DiscoveryProviderManager(mContext, mExecutor, mInjector,
+ mBleDiscoveryProvider,
+ mChreDiscoveryProvider);
+ mCallerIdentity = CallerIdentity
+ .forTest(UID, PID, PACKAGE_NAME, /* attributionTag= */ null);
+ }
+
+ @Test
+ public void testOnNearbyDeviceDiscovered() {
+ NearbyDeviceParcelable nearbyDeviceParcelable = new NearbyDeviceParcelable.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .build();
+ mDiscoveryProviderManager.onNearbyDeviceDiscovered(nearbyDeviceParcelable);
+ }
+
+ @Test
+ public void testInvalidateProviderScanMode() {
+ mDiscoveryProviderManager.invalidateProviderScanMode();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
+ reset(mBluetoothController);
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ Boolean start = mDiscoveryProviderManager.startProviders();
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreAvailable_multipleFilters_bleProviderNotStarted() {
+ reset(mBluetoothController);
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ Boolean start = mDiscoveryProviderManager.startProviders();
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreUnavailable_bleProviderNotStarted() {
+ reset(mBluetoothController);
+ when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ Boolean start = mDiscoveryProviderManager.startProviders();
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isFalse();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreAvailable_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+ reset(mBluetoothController);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ Boolean start = mDiscoveryProviderManager.startProviders();
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreUnavailable_bleProviderStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(false);
+ reset(mBluetoothController);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ Boolean start = mDiscoveryProviderManager.startProviders();
+ verify(mBluetoothController, atLeastOnce()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreUndetermined_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(null);
+ reset(mBluetoothController);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ Boolean start = mDiscoveryProviderManager.startProviders();
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isNull();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreUndetermined_bleProviderStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(null);
+ reset(mBluetoothController);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ Boolean start = mDiscoveryProviderManager.startProviders();
+ verify(mBluetoothController, atLeastOnce()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void test_stopChreProvider_clearFilters() throws Exception {
+ // Cannot use mocked ChreDiscoveryProvider,
+ // so we cannot use class variable mDiscoveryProviderManager
+ DiscoveryProviderManager manager =
+ new DiscoveryProviderManager(mContext, mExecutor, mInjector,
+ mBleDiscoveryProvider,
+ new ChreDiscoveryProvider(
+ mContext,
+ new ChreCommunication(mInjector, mContext, mExecutor), mExecutor));
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ manager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ // This is an asynchronized process. The filters will be set in executor thread. So we need
+ // to wait for some time to get the correct result.
+ Thread.sleep(200);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ manager.stopChreProvider();
+ Thread.sleep(200);
+ // The filters should be cleared right after.
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isFalse();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isEmpty();
+ }
+
+ @Test
+ public void test_restartChreProvider() throws Exception {
+ // Cannot use mocked ChreDiscoveryProvider,
+ // so we cannot use class variable mDiscoveryProviderManager
+ DiscoveryProviderManager manager =
+ new DiscoveryProviderManager(mContext, mExecutor, mInjector,
+ mBleDiscoveryProvider,
+ new ChreDiscoveryProvider(
+ mContext,
+ new ChreCommunication(mInjector, mContext, mExecutor), mExecutor));
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ manager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ // This is an asynchronized process. The filters will be set in executor thread. So we need
+ // to wait for some time to get the correct result.
+ Thread.sleep(200);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ // We want to make sure quickly restart the provider the filters should
+ // be reset correctly.
+ // See b/255922206, there can be a race condition that filters get cleared because onStop()
+ // get executed after onStart() if they are called from different threads.
+ manager.stopChreProvider();
+ manager.mChreDiscoveryProvider.getController().setProviderScanFilters(
+ List.of(getPresenceScanFilter()));
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ Thread.sleep(200);
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ // Wait for enough time
+ Thread.sleep(1000);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/ListenerMultiplexerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/ListenerMultiplexerTest.java
new file mode 100644
index 0000000..104d762
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/ListenerMultiplexerTest.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.managers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.nearby.managers.registration.BinderListenerRegistration;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+
+public class ListenerMultiplexerTest {
+
+ @Before
+ public void setUp() {
+ initMocks(this);
+ }
+
+ @Test
+ public void testAdd() {
+ TestMultiplexer multiplexer = new TestMultiplexer();
+
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+ multiplexer.addListener(binder, listener, value);
+
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ Runnable listener2 = mock(Runnable.class);
+ IBinder binder2 = mock(IBinder.class);
+ int value2 = 1;
+ multiplexer.addListener(binder2, listener2, value2);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(2);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ }
+
+ @Test
+ public void testReplace() {
+ TestMultiplexer multiplexer = new TestMultiplexer();
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+ multiplexer.addListener(binder, listener, value);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ reset(listener);
+
+ // Same key, different value
+ Runnable listener2 = mock(Runnable.class);
+ int value2 = 1;
+ multiplexer.addListener(binder, listener2, value2);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ // Should not be called again
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mOnUnregisterCalledCount).isEqualTo(0);
+ assertThat(multiplexer.mMerged).isEqualTo(value2);
+ }
+ // Run on the new listener
+ multiplexer.notifyListeners();
+ verify(listener, never()).run();
+ verify(listener2, times(1)).run();
+
+ multiplexer.removeRegistration(binder);
+
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isFalse();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mOnUnregisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+ }
+ }
+
+ @Test
+ public void testRemove() {
+ TestMultiplexer multiplexer = new TestMultiplexer();
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+ multiplexer.addListener(binder, listener, value);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ reset(listener);
+
+ multiplexer.removeRegistration(binder);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isFalse();
+ assertThat(multiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, never()).run();
+ }
+
+ @Test
+ public void testMergeMultiple() {
+ TestMultiplexer multiplexer = new TestMultiplexer();
+
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+
+ Runnable listener2 = mock(Runnable.class);
+ IBinder binder2 = mock(IBinder.class);
+ int value2 = 1;
+
+ Runnable listener3 = mock(Runnable.class);
+ IBinder binder3 = mock(IBinder.class);
+ int value3 = 5;
+
+ multiplexer.addListener(binder, listener, value);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ verify(listener2, never()).run();
+ verify(listener3, never()).run();
+
+ multiplexer.addListener(binder2, listener2, value2);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(2);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(2)).run();
+ verify(listener2, times(1)).run();
+ verify(listener3, never()).run();
+
+ multiplexer.addListener(binder3, listener3, value3);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(3);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(2);
+ assertThat(multiplexer.mMerged).isEqualTo(value3);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(3)).run();
+ verify(listener2, times(2)).run();
+ verify(listener3, times(1)).run();
+
+ multiplexer.removeRegistration(binder);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(4);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(2);
+ assertThat(multiplexer.mMerged).isEqualTo(value3);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(3)).run();
+ verify(listener2, times(3)).run();
+ verify(listener3, times(2)).run();
+
+ multiplexer.removeRegistration(binder3);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(5);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(3);
+ assertThat(multiplexer.mMerged).isEqualTo(value2);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(3)).run();
+ verify(listener2, times(4)).run();
+ verify(listener3, times(2)).run();
+
+ multiplexer.removeRegistration(binder2);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isFalse();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(6);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(4);
+ assertThat(multiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(3)).run();
+ verify(listener2, times(4)).run();
+ verify(listener3, times(2)).run();
+ }
+
+ private class TestMultiplexer extends
+ ListenerMultiplexer<Runnable, TestMultiplexer.TestListenerRegistration, Integer> {
+ int mOnRegisterCalledCount;
+ int mOnUnregisterCalledCount;
+ boolean mRegistered;
+ private int mMergeOperationCount;
+ private int mMergeUpdatedCount;
+
+ @Override
+ public void onRegister() {
+ mOnRegisterCalledCount++;
+ mRegistered = true;
+ }
+
+ @Override
+ public void onUnregister() {
+ mOnUnregisterCalledCount++;
+ mRegistered = false;
+ }
+
+ @Override
+ public Integer mergeRegistrations(
+ @NonNull Collection<TestListenerRegistration> testListenerRegistrations) {
+ mMergeOperationCount++;
+ int max = Integer.MIN_VALUE;
+ for (TestListenerRegistration registration : testListenerRegistrations) {
+ max = Math.max(max, registration.getValue());
+ }
+ return max;
+ }
+
+ @Override
+ public void onMergedRegistrationsUpdated() {
+ mMergeUpdatedCount++;
+ }
+
+ public void addListener(IBinder binder, Runnable runnable, int value) {
+ TestListenerRegistration registration = new TestListenerRegistration(binder, runnable,
+ value);
+ putRegistration(binder, registration);
+ }
+
+ public void notifyListeners() {
+ deliverToListeners(registration -> Runnable::run);
+ }
+
+ private class TestListenerRegistration extends BinderListenerRegistration<Runnable> {
+ private final int mValue;
+
+ protected TestListenerRegistration(IBinder binder, Runnable runnable, int value) {
+ super(binder, MoreExecutors.directExecutor(), runnable);
+ mValue = value;
+ }
+
+ @Override
+ public TestMultiplexer getOwner() {
+ return TestMultiplexer.this;
+ }
+
+ public int getValue() {
+ return mValue;
+ }
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/MergedDiscoveryRequestTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/MergedDiscoveryRequestTest.java
new file mode 100644
index 0000000..9281e42
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/MergedDiscoveryRequestTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.DataElement;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.util.ArraySet;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Unit test for {@link MergedDiscoveryRequest} class.
+ */
+public class MergedDiscoveryRequestTest {
+
+ @Test
+ public void test_addScanType() {
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ builder.addScanType(ScanRequest.SCAN_TYPE_FAST_PAIR);
+ builder.addScanType(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE);
+ MergedDiscoveryRequest request = builder.build();
+
+ assertThat(request.getScanTypes()).isEqualTo(new ArraySet<>(
+ Arrays.asList(ScanRequest.SCAN_TYPE_FAST_PAIR,
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE)));
+ }
+
+ @Test
+ public void test_addActions() {
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ builder.addActions(new ArrayList<>(Arrays.asList(1, 2, 3)));
+ builder.addActions(new ArraySet<>(Arrays.asList(2, 3, 4)));
+ builder.addActions(new ArraySet<>(Collections.singletonList(5)));
+
+ MergedDiscoveryRequest request = builder.build();
+ assertThat(request.getActions()).isEqualTo(new ArraySet<>(new Integer[]{1, 2, 3, 4, 5}));
+ }
+
+ @Test
+ public void test_addFilters() {
+ final int rssi = -40;
+ final int action = 123;
+ final byte[] secreteId = new byte[]{1, 2, 3, 4};
+ final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+ final byte[] publicKey = new byte[]{1, 1, 2, 2};
+ final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+ final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+ final int key = 3;
+ final byte[] value = new byte[]{1, 1, 1, 1};
+
+ PublicCredential mPublicCredential = new PublicCredential.Builder(secreteId,
+ authenticityKey, publicKey, encryptedMetadata,
+ metadataEncryptionKeyTag).setIdentityType(IDENTITY_TYPE_PRIVATE).build();
+ PresenceScanFilter scanFilterBuilder = new PresenceScanFilter.Builder().setMaxPathLoss(
+ rssi).addCredential(mPublicCredential).addPresenceAction(
+ action).addExtendedProperty(new DataElement(key, value)).build();
+
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ builder.addScanFilters(Collections.singleton(scanFilterBuilder));
+ MergedDiscoveryRequest request = builder.build();
+
+ Set<ScanFilter> expectedResult = new ArraySet<>();
+ expectedResult.add(scanFilterBuilder);
+ assertThat(request.getScanFilters()).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void test_addMedium() {
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ builder.addMedium(MergedDiscoveryRequest.Medium.BLE);
+ builder.addMedium(MergedDiscoveryRequest.Medium.BLE);
+ MergedDiscoveryRequest request = builder.build();
+
+ Set<Integer> expectedResult = new ArraySet<>();
+ expectedResult.add(MergedDiscoveryRequest.Medium.BLE);
+ assertThat(request.getMediums()).isEqualTo(expectedResult);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/registration/BinderListenerRegistrationTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/BinderListenerRegistrationTest.java
new file mode 100644
index 0000000..8814190
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/BinderListenerRegistrationTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.managers.registration;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.nearby.managers.ListenerMultiplexer;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+
+/**
+ * Unit test for {@link BinderListenerRegistration} class.
+ */
+public class BinderListenerRegistrationTest {
+ private TestMultiplexer mMultiplexer;
+ private boolean mOnRegisterCalled;
+ private boolean mOnUnRegisterCalled;
+
+ @Before
+ public void setUp() {
+ mMultiplexer = new TestMultiplexer();
+ }
+
+ @Test
+ public void test_addAndRemove() throws RemoteException {
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+ BinderListenerRegistration<Runnable> registration = mMultiplexer.addListener(binder,
+ listener, value);
+ // First element, onRegister should be called
+ assertThat(mOnRegisterCalled).isTrue();
+ verify(binder, times(1)).linkToDeath(any(), anyInt());
+ mMultiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ synchronized (mMultiplexer.mMultiplexerLock) {
+ assertThat(mMultiplexer.mMerged).isEqualTo(value);
+ }
+ reset(listener);
+
+ Runnable listener2 = mock(Runnable.class);
+ IBinder binder2 = mock(IBinder.class);
+ int value2 = 1;
+ BinderListenerRegistration<Runnable> registration2 = mMultiplexer.addListener(binder2,
+ listener2, value2);
+ verify(binder2, times(1)).linkToDeath(any(), anyInt());
+ mMultiplexer.notifyListeners();
+ verify(listener2, times(1)).run();
+ synchronized (mMultiplexer.mMultiplexerLock) {
+ assertThat(mMultiplexer.mMerged).isEqualTo(value);
+ }
+ reset(listener);
+ reset(listener2);
+
+ registration2.remove();
+ verify(binder2, times(1)).unlinkToDeath(any(), anyInt());
+ // Remove one element, onUnregister should NOT be called
+ assertThat(mOnUnRegisterCalled).isFalse();
+ mMultiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ synchronized (mMultiplexer.mMultiplexerLock) {
+ assertThat(mMultiplexer.mMerged).isEqualTo(value);
+ }
+ reset(listener);
+ reset(listener2);
+
+ registration.remove();
+ verify(binder, times(1)).unlinkToDeath(any(), anyInt());
+ // Remove all elements, onUnregister should NOT be called
+ assertThat(mOnUnRegisterCalled).isTrue();
+ synchronized (mMultiplexer.mMultiplexerLock) {
+ assertThat(mMultiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+ }
+ }
+
+ private class TestMultiplexer extends
+ ListenerMultiplexer<Runnable, TestMultiplexer.TestListenerRegistration, Integer> {
+ @Override
+ public void onRegister() {
+ mOnRegisterCalled = true;
+ }
+
+ @Override
+ public void onUnregister() {
+ mOnUnRegisterCalled = true;
+ }
+
+ @Override
+ public Integer mergeRegistrations(
+ @NonNull Collection<TestListenerRegistration> testListenerRegistrations) {
+ int max = Integer.MIN_VALUE;
+ for (TestListenerRegistration registration : testListenerRegistrations) {
+ max = Math.max(max, registration.getValue());
+ }
+ return max;
+ }
+
+ @Override
+ public void onMergedRegistrationsUpdated() {
+ }
+
+ public BinderListenerRegistration<Runnable> addListener(IBinder binder, Runnable runnable,
+ int value) {
+ TestListenerRegistration registration = new TestListenerRegistration(binder, runnable,
+ value);
+ putRegistration(binder, registration);
+ return registration;
+ }
+
+ public void notifyListeners() {
+ deliverToListeners(registration -> Runnable::run);
+ }
+
+ private class TestListenerRegistration extends BinderListenerRegistration<Runnable> {
+ private final int mValue;
+
+ protected TestListenerRegistration(IBinder binder, Runnable runnable, int value) {
+ super(binder, MoreExecutors.directExecutor(), runnable);
+ mValue = value;
+ }
+
+ @Override
+ public TestMultiplexer getOwner() {
+ return TestMultiplexer.this;
+ }
+
+ public int getValue() {
+ return mValue;
+ }
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/registration/DiscoveryRegistrationTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/DiscoveryRegistrationTest.java
new file mode 100644
index 0000000..03c4f75
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/DiscoveryRegistrationTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.managers.registration;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.app.AppOpsManager;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanCallback;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.os.IBinder;
+import android.util.ArraySet;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.nearby.managers.ListenerMultiplexer;
+import com.android.server.nearby.managers.MergedDiscoveryRequest;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executors;
+
+/**
+ * Unit test for {@link DiscoveryRegistration} class.
+ */
+public class DiscoveryRegistrationTest {
+ private static final int RSSI = -40;
+ private static final int ACTION = 123;
+ private static final byte[] SECRETE_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
+ private static final byte[] PUBLIC_KEY = new byte[]{1, 1, 2, 2};
+ private static final byte[] ENCRYPTED_METADATA = new byte[]{1, 2, 3, 4, 5};
+ private static final byte[] METADATA_ENCRYPTION_KEY_TAG = new byte[]{1, 1, 3, 4, 5};
+ private static final int KEY = 3;
+ private static final byte[] VALUE = new byte[]{1, 1, 1, 1};
+ private final PublicCredential mPublicCredential = new PublicCredential.Builder(SECRETE_ID,
+ AUTHENTICITY_KEY, PUBLIC_KEY, ENCRYPTED_METADATA,
+ METADATA_ENCRYPTION_KEY_TAG).setIdentityType(IDENTITY_TYPE_PRIVATE).build();
+ private final PresenceScanFilter mFilter = new PresenceScanFilter.Builder().setMaxPathLoss(
+ 50).addCredential(mPublicCredential).addPresenceAction(ACTION).addExtendedProperty(
+ new DataElement(KEY, VALUE)).build();
+ private DiscoveryRegistration mDiscoveryRegistration;
+ private ScanRequest mScanRequest;
+ private TestDiscoveryManager mOwner;
+ private Object mMultiplexLock;
+ @Mock
+ private IScanListener mCallback;
+ @Mock
+ private CallerIdentity mIdentity;
+ @Mock
+ private AppOpsManager mAppOpsManager;
+ @Mock
+ private IBinder mBinder;
+
+ @Before
+ public void setUp() {
+ initMocks(this);
+ when(mCallback.asBinder()).thenReturn(mBinder);
+ when(mAppOpsManager.noteOp(eq("android:bluetooth_scan"), eq(0), eq(null), eq(null),
+ eq(null))).thenReturn(AppOpsManager.MODE_ALLOWED);
+
+ mOwner = new TestDiscoveryManager();
+ mMultiplexLock = new Object();
+ mScanRequest = new ScanRequest.Builder().setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).addScanFilter(mFilter).build();
+ mDiscoveryRegistration = new DiscoveryRegistration(mOwner, mScanRequest, mCallback,
+ Executors.newSingleThreadExecutor(), mIdentity, mMultiplexLock, mAppOpsManager);
+ }
+
+ @Test
+ public void test_getScanRequest() {
+ assertThat(mDiscoveryRegistration.getScanRequest()).isEqualTo(mScanRequest);
+ }
+
+ @Test
+ public void test_getActions() {
+ Set<Integer> result = new ArraySet<>();
+ result.add(ACTION);
+ assertThat(mDiscoveryRegistration.getActions()).isEqualTo(result);
+ }
+
+ @Test
+ public void test_getOwner() {
+ assertThat(mDiscoveryRegistration.getOwner()).isEqualTo(mOwner);
+ }
+
+ @Test
+ public void test_getPresenceScanFilters() {
+ Set<ScanFilter> result = new ArraySet<>();
+ result.add(mFilter);
+ assertThat(mDiscoveryRegistration.getPresenceScanFilters()).isEqualTo(result);
+ }
+
+ @Test
+ public void test_presenceFilterMatches_match() {
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ 123).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG).build();
+ assertThat(DiscoveryRegistration.presenceFilterMatches(device, List.of(mFilter))).isTrue();
+ }
+
+ @Test
+ public void test_presenceFilterMatches_emptyFilter() {
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ 123).setName("test").setScanType(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).build();
+ assertThat(DiscoveryRegistration.presenceFilterMatches(device, List.of())).isTrue();
+ }
+
+ @Test
+ public void test_presenceFilterMatches_actionNotMatch() {
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ 12).setName("test").setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(5).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG).build();
+ assertThat(DiscoveryRegistration.presenceFilterMatches(device, List.of(mFilter))).isFalse();
+ }
+
+ @Test
+ public void test_onDiscoveredOnUpdatedCalled() throws Exception {
+ final long deviceId = 122;
+ NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder().setDeviceId(
+ deviceId).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG);
+ runOperation(mDiscoveryRegistration.onNearbyDeviceDiscovered(builder.build()));
+
+ verify(mCallback, times(1)).onDiscovered(eq(builder.build()));
+ verify(mCallback, never()).onUpdated(any());
+ verify(mCallback, never()).onLost(any());
+ verify(mCallback, never()).onError(anyInt());
+ assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNotNull();
+ reset(mCallback);
+
+ // Update RSSI
+ runOperation(
+ mDiscoveryRegistration.onNearbyDeviceDiscovered(builder.setRssi(RSSI - 1).build()));
+ verify(mCallback, never()).onDiscovered(any());
+ verify(mCallback, times(1)).onUpdated(eq(builder.build()));
+ verify(mCallback, never()).onLost(any());
+ verify(mCallback, never()).onError(anyInt());
+ assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNotNull();
+ }
+
+ @Test
+ public void test_onLost() throws Exception {
+ final long deviceId = 123;
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ deviceId).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG).build();
+ runOperation(mDiscoveryRegistration.onNearbyDeviceDiscovered(device));
+ assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNotNull();
+ verify(mCallback, times(1)).onDiscovered(eq(device));
+ verify(mCallback, never()).onUpdated(any());
+ verify(mCallback, never()).onError(anyInt());
+ verify(mCallback, never()).onLost(any());
+ reset(mCallback);
+
+ runOperation(mDiscoveryRegistration.reportDeviceLost(device));
+
+ assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNull();
+ verify(mCallback, never()).onDiscovered(eq(device));
+ verify(mCallback, never()).onUpdated(any());
+ verify(mCallback, never()).onError(anyInt());
+ verify(mCallback, times(1)).onLost(eq(device));
+ }
+
+ @Test
+ public void test_onError() throws Exception {
+ AppOpsManager manager = mock(AppOpsManager.class);
+ when(manager.noteOp(eq("android:bluetooth_scan"), eq(0), eq(null), eq(null),
+ eq(null))).thenReturn(AppOpsManager.MODE_IGNORED);
+
+ DiscoveryRegistration r = new DiscoveryRegistration(mOwner, mScanRequest, mCallback,
+ Executors.newSingleThreadExecutor(), mIdentity, mMultiplexLock, manager);
+
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ 123).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG).build();
+ runOperation(r.onNearbyDeviceDiscovered(device));
+
+ verify(mCallback, never()).onDiscovered(any());
+ verify(mCallback, never()).onUpdated(any());
+ verify(mCallback, never()).onLost(any());
+ verify(mCallback, times(1)).onError(eq(ScanCallback.ERROR_PERMISSION_DENIED));
+ }
+
+ private void runOperation(BinderListenerRegistration.ListenerOperation<IScanListener> operation)
+ throws Exception {
+ if (operation == null) {
+ return;
+ }
+ operation.onScheduled(false);
+ operation.operate(mCallback);
+ operation.onComplete(/* success= */ true);
+ }
+
+ private static class TestDiscoveryManager extends
+ ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> {
+
+ @Override
+ public MergedDiscoveryRequest mergeRegistrations(
+ @NonNull Collection<DiscoveryRegistration> discoveryRegistrations) {
+ return null;
+ }
+
+ @Override
+ public void onMergedRegistrationsUpdated() {
+
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/DataElementHeaderTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/DataElementHeaderTest.java
new file mode 100644
index 0000000..e186709
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/DataElementHeaderTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.nearby.BroadcastRequest;
+
+import org.junit.Test;
+
+import java.util.List;
+
+/**
+ * Unit test for {@link DataElementHeader}.
+ */
+public class DataElementHeaderTest {
+
+ private static final int VERSION = BroadcastRequest.PRESENCE_VERSION_V1;
+
+ @Test
+ public void test_illegalLength() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new DataElementHeader(VERSION, 12, 128));
+ }
+
+ @Test
+ public void test_singeByteConversion() {
+ DataElementHeader header = new DataElementHeader(VERSION, 12, 3);
+ byte[] bytes = header.toBytes();
+ assertThat(bytes).isEqualTo(new byte[]{(byte) 0b00111100});
+
+ DataElementHeader afterConversionHeader = DataElementHeader.fromBytes(VERSION, bytes);
+ assertThat(afterConversionHeader.getDataLength()).isEqualTo(3);
+ assertThat(afterConversionHeader.getDataType()).isEqualTo(12);
+ }
+
+ @Test
+ public void test_multipleBytesConversion() {
+ DataElementHeader header = new DataElementHeader(VERSION, 6, 100);
+ DataElementHeader afterConversionHeader =
+ DataElementHeader.fromBytes(VERSION, header.toBytes());
+ assertThat(afterConversionHeader.getDataLength()).isEqualTo(100);
+ assertThat(afterConversionHeader.getDataType()).isEqualTo(6);
+ }
+
+ @Test
+ public void test_fromBytes() {
+ // Single byte case.
+ byte[] singleByte = new byte[]{(byte) 0b01011101};
+ DataElementHeader singeByteHeader = DataElementHeader.fromBytes(VERSION, singleByte);
+ assertThat(singeByteHeader.getDataLength()).isEqualTo(5);
+ assertThat(singeByteHeader.getDataType()).isEqualTo(13);
+
+ // Two bytes case.
+ byte[] twoBytes = new byte[]{(byte) 0b11011101, (byte) 0b01011101};
+ DataElementHeader twoBytesHeader = DataElementHeader.fromBytes(VERSION, twoBytes);
+ assertThat(twoBytesHeader.getDataLength()).isEqualTo(93);
+ assertThat(twoBytesHeader.getDataType()).isEqualTo(93);
+
+ // Three bytes case.
+ byte[] threeBytes = new byte[]{(byte) 0b11011101, (byte) 0b11111111, (byte) 0b01011101};
+ DataElementHeader threeBytesHeader = DataElementHeader.fromBytes(VERSION, threeBytes);
+ assertThat(threeBytesHeader.getDataLength()).isEqualTo(93);
+ assertThat(threeBytesHeader.getDataType()).isEqualTo(16349);
+
+ // Four bytes case.
+ byte[] fourBytes = new byte[]{
+ (byte) 0b11011101, (byte) 0b11111111, (byte) 0b11111111, (byte) 0b01011101};
+
+ DataElementHeader fourBytesHeader = DataElementHeader.fromBytes(VERSION, fourBytes);
+ assertThat(fourBytesHeader.getDataLength()).isEqualTo(93);
+ assertThat(fourBytesHeader.getDataType()).isEqualTo(2097117);
+ }
+
+ @Test
+ public void test_fromBytesIllegal_singleByte() {
+ assertThrows(IllegalArgumentException.class,
+ () -> DataElementHeader.fromBytes(VERSION, new byte[]{(byte) 0b11011101}));
+ }
+
+ @Test
+ public void test_fromBytesIllegal_twoBytes_wrongFirstByte() {
+ assertThrows(IllegalArgumentException.class,
+ () -> DataElementHeader.fromBytes(VERSION,
+ new byte[]{(byte) 0b01011101, (byte) 0b01011101}));
+ }
+
+ @Test
+ public void test_fromBytesIllegal_twoBytes_wrongLastByte() {
+ assertThrows(IllegalArgumentException.class,
+ () -> DataElementHeader.fromBytes(VERSION,
+ new byte[]{(byte) 0b11011101, (byte) 0b11011101}));
+ }
+
+ @Test
+ public void test_fromBytesIllegal_threeBytes() {
+ assertThrows(IllegalArgumentException.class,
+ () -> DataElementHeader.fromBytes(VERSION,
+ new byte[]{(byte) 0b11011101, (byte) 0b11011101, (byte) 0b11011101}));
+ }
+
+ @Test
+ public void test_multipleBytesConversion_largeNumber() {
+ DataElementHeader header = new DataElementHeader(VERSION, 22213546, 66);
+ DataElementHeader afterConversionHeader =
+ DataElementHeader.fromBytes(VERSION, header.toBytes());
+ assertThat(afterConversionHeader.getDataLength()).isEqualTo(66);
+ assertThat(afterConversionHeader.getDataType()).isEqualTo(22213546);
+ }
+
+ @Test
+ public void test_isExtending() {
+ assertThat(DataElementHeader.isExtending((byte) 0b10000100)).isTrue();
+ assertThat(DataElementHeader.isExtending((byte) 0b01110100)).isFalse();
+ assertThat(DataElementHeader.isExtending((byte) 0b00000000)).isFalse();
+ }
+
+ @Test
+ public void test_convertTag() {
+ assertThat(DataElementHeader.convertTag(true)).isEqualTo((byte) 128);
+ assertThat(DataElementHeader.convertTag(false)).isEqualTo(0);
+ }
+
+ @Test
+ public void test_getHeaderValue() {
+ assertThat(DataElementHeader.getHeaderValue((byte) 0b10000100)).isEqualTo(4);
+ assertThat(DataElementHeader.getHeaderValue((byte) 0b00000100)).isEqualTo(4);
+ assertThat(DataElementHeader.getHeaderValue((byte) 0b11010100)).isEqualTo(84);
+ assertThat(DataElementHeader.getHeaderValue((byte) 0b01010100)).isEqualTo(84);
+ }
+
+ @Test
+ public void test_convertTypeMultipleIntList() {
+ List<Byte> list = DataElementHeader.convertTypeMultipleBytes(128);
+ assertThat(list.size()).isEqualTo(2);
+ assertThat(list.get(0)).isEqualTo((byte) 0b10000001);
+ assertThat(list.get(1)).isEqualTo((byte) 0b00000000);
+
+ List<Byte> list2 = DataElementHeader.convertTypeMultipleBytes(10);
+ assertThat(list2.size()).isEqualTo(1);
+ assertThat(list2.get(0)).isEqualTo((byte) 0b00001010);
+
+ List<Byte> list3 = DataElementHeader.convertTypeMultipleBytes(5242398);
+ assertThat(list3.size()).isEqualTo(4);
+ assertThat(list3.get(0)).isEqualTo((byte) 0b10000010);
+ assertThat(list3.get(1)).isEqualTo((byte) 0b10111111);
+ assertThat(list3.get(2)).isEqualTo((byte) 0b11111100);
+ assertThat(list3.get(3)).isEqualTo((byte) 0b00011110);
+ }
+
+ @Test
+ public void test_getTypeMultipleBytes() {
+ byte[] inputBytes = new byte[]{(byte) 0b11011000, (byte) 0b10000000, (byte) 0b00001001};
+ // 0b101100000000000001001
+ assertThat(DataElementHeader.getTypeMultipleBytes(inputBytes)).isEqualTo(1441801);
+
+ byte[] inputBytes2 = new byte[]{(byte) 0b00010010};
+ assertThat(DataElementHeader.getTypeMultipleBytes(inputBytes2)).isEqualTo(18);
+
+ byte[] inputBytes3 = new byte[]{(byte) 0b10000001, (byte) 0b00000000};
+ assertThat(DataElementHeader.getTypeMultipleBytes(inputBytes3)).isEqualTo(128);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
new file mode 100644
index 0000000..895df69
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceCredential;
+import android.nearby.PrivateCredential;
+import android.nearby.PublicCredential;
+
+import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
+import com.android.server.nearby.util.encryption.CryptorImpV1;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ExtendedAdvertisementTest {
+ private static final int IDENTITY_TYPE = PresenceCredential.IDENTITY_TYPE_PRIVATE;
+ private static final int DATA_TYPE_MODEL_ID = 7;
+ private static final int DATA_TYPE_BLE_ADDRESS = 101;
+ private static final int DATA_TYPE_PUBLIC_IDENTITY = 3;
+ private static final byte[] MODE_ID_DATA =
+ new byte[]{2, 1, 30, 2, 10, -16, 6, 22, 44, -2, -86, -69, -52};
+ private static final byte[] BLE_ADDRESS = new byte[]{124, 4, 56, 60, 120, -29, -90};
+ private static final DataElement MODE_ID_ADDRESS_ELEMENT =
+ new DataElement(DATA_TYPE_MODEL_ID, MODE_ID_DATA);
+ private static final DataElement BLE_ADDRESS_ELEMENT =
+ new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS);
+
+ private static final byte[] IDENTITY =
+ new byte[]{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4};
+ private static final int MEDIUM_TYPE_BLE = 0;
+ private static final byte[] SALT = {2, 3};
+ private static final int PRESENCE_ACTION_1 = 1;
+ private static final int PRESENCE_ACTION_2 = 2;
+
+ private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] AUTHENTICITY_KEY =
+ new byte[]{-97, 10, 107, -86, 25, 65, -54, -95, -72, 59, 54, 93, 9, 3, -24, -88};
+ private static final byte[] PUBLIC_KEY =
+ new byte[] {
+ 48, 89, 48, 19, 6, 7, 42, -122, 72, -50, 61, 2, 1, 6, 8, 42, -122, 72, -50, 61,
+ 66, 0, 4, -56, -39, -92, 69, 0, 52, 23, 67, 83, -14, 75, 52, -14, -5, -41, 48,
+ -83, 31, 42, -39, 102, -13, 22, -73, -73, 86, 30, -96, -84, -13, 4, 122, 104,
+ -65, 64, 91, -109, -45, -35, -56, 55, -79, 47, -85, 27, -96, -119, -82, -80,
+ 123, 41, -119, -25, 1, -112, 112
+ };
+ private static final byte[] ENCRYPTED_METADATA_BYTES =
+ new byte[] {
+ -44, -25, -95, -124, -7, 90, 116, -8, 7, -120, -23, -22, -106, -44, -19, 61,
+ -18, 39, 29, 78, 108, -11, -39, 85, -30, 64, -99, 102, 65, 37, -42, 114, -37,
+ 88, -112, 8, -75, -53, 23, -16, -104, 67, 49, 48, -53, 73, -109, 44, -23, -11,
+ -118, -61, -37, -104, 60, 105, 115, 1, 56, -89, -107, -45, -116, -1, -25, 84,
+ -19, -128, 81, 11, 92, 77, -58, 82, 122, 123, 31, -87, -57, 70, 23, -81, 7, 2,
+ -114, -83, 74, 124, -68, -98, 47, 91, 9, 48, -67, 41, -7, -97, 78, 66, -65, 58,
+ -4, -46, -30, -85, -50, 100, 46, -66, -128, 7, 66, 9, 88, 95, 12, -13, 81, -91,
+ };
+ private static final byte[] METADATA_ENCRYPTION_KEY_TAG =
+ new byte[] {-126, -104, 1, -1, 26, -46, -68, -86};
+ private static final String DEVICE_NAME = "test_device";
+
+ private PresenceBroadcastRequest.Builder mBuilder;
+ private PrivateCredential mPrivateCredential;
+ private PublicCredential mPublicCredential;
+
+ @Before
+ public void setUp() {
+ mPrivateCredential =
+ new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, IDENTITY, DEVICE_NAME)
+ .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+ .build();
+ mPublicCredential =
+ new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
+ ENCRYPTED_METADATA_BYTES, METADATA_ENCRYPTION_KEY_TAG)
+ .build();
+ mBuilder =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ SALT, mPrivateCredential)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+ .addAction(PRESENCE_ACTION_1)
+ .addAction(PRESENCE_ACTION_2)
+ .addExtendedProperty(new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS))
+ .addExtendedProperty(new DataElement(DATA_TYPE_MODEL_ID, MODE_ID_DATA));
+ }
+
+ @Test
+ public void test_createFromRequest() {
+ ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
+ mBuilder.build());
+
+ assertThat(originalAdvertisement.getActions())
+ .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
+ assertThat(originalAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+ assertThat(originalAdvertisement.getLength()).isEqualTo(66);
+ assertThat(originalAdvertisement.getVersion()).isEqualTo(
+ BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
+ assertThat(originalAdvertisement.getDataElements())
+ .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ }
+
+ @Test
+ public void test_createFromRequest_encodeAndDecode() {
+ ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
+ mBuilder.build());
+
+ byte[] generatedBytes = originalAdvertisement.toBytes();
+
+ ExtendedAdvertisement newAdvertisement =
+ ExtendedAdvertisement.fromBytes(generatedBytes, mPublicCredential);
+
+ assertThat(newAdvertisement.getActions())
+ .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
+ assertThat(newAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(newAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+ assertThat(newAdvertisement.getLength()).isEqualTo(66);
+ assertThat(newAdvertisement.getVersion()).isEqualTo(
+ BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(newAdvertisement.getSalt()).isEqualTo(SALT);
+ assertThat(newAdvertisement.getDataElements())
+ .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ }
+
+ @Test
+ public void test_createFromRequest_invalidParameter() {
+ // invalid version
+ mBuilder.setVersion(BroadcastRequest.PRESENCE_VERSION_V0);
+ assertThat(ExtendedAdvertisement.createFromRequest(mBuilder.build())).isNull();
+
+ // invalid salt
+ PresenceBroadcastRequest.Builder builder =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ new byte[]{1, 2, 3}, mPrivateCredential)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+ .addAction(PRESENCE_ACTION_1);
+ assertThat(ExtendedAdvertisement.createFromRequest(builder.build())).isNull();
+
+ // invalid identity
+ PrivateCredential privateCredential =
+ new PrivateCredential.Builder(SECRET_ID,
+ AUTHENTICITY_KEY, new byte[]{1, 2, 3, 4}, DEVICE_NAME)
+ .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+ .build();
+ PresenceBroadcastRequest.Builder builder2 =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ new byte[]{1, 2, 3}, privateCredential)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+ .addAction(PRESENCE_ACTION_1);
+ assertThat(ExtendedAdvertisement.createFromRequest(builder2.build())).isNull();
+
+ // empty action
+ PresenceBroadcastRequest.Builder builder3 =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ SALT, mPrivateCredential)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(ExtendedAdvertisement.createFromRequest(builder3.build())).isNull();
+ }
+
+ @Test
+ public void test_toBytes() {
+ ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
+ assertThat(adv.toBytes()).isEqualTo(getExtendedAdvertisementByteArray());
+ }
+
+ @Test
+ public void test_fromBytes() {
+ byte[] originalBytes = getExtendedAdvertisementByteArray();
+ ExtendedAdvertisement adv =
+ ExtendedAdvertisement.fromBytes(originalBytes, mPublicCredential);
+
+ assertThat(adv.getActions())
+ .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
+ assertThat(adv.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(adv.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+ assertThat(adv.getLength()).isEqualTo(66);
+ assertThat(adv.getVersion()).isEqualTo(
+ BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(adv.getSalt()).isEqualTo(SALT);
+ assertThat(adv.getDataElements())
+ .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ }
+
+ @Test
+ public void test_toString() {
+ ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
+ assertThat(adv.toString()).isEqualTo("ExtendedAdvertisement:"
+ + "<VERSION: 1, length: 66, dataElementCount: 2, identityType: 1, "
+ + "identity: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], salt: [2, 3],"
+ + " actions: [1, 2]>");
+ }
+
+ @Test
+ public void test_getDataElements_accordingToType() {
+ ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
+ List<DataElement> dataElements = new ArrayList<>();
+
+ dataElements.add(BLE_ADDRESS_ELEMENT);
+ assertThat(adv.getDataElements(DATA_TYPE_BLE_ADDRESS)).isEqualTo(dataElements);
+ assertThat(adv.getDataElements(DATA_TYPE_PUBLIC_IDENTITY)).isEmpty();
+ }
+
+ private static byte[] getExtendedAdvertisementByteArray() {
+ ByteBuffer buffer = ByteBuffer.allocate(66);
+ buffer.put((byte) 0b00100000); // Header V1
+ buffer.put((byte) 0b00100000); // Salt header: length 2, type 0
+ // Salt data
+ buffer.put(SALT);
+ // Identity header: length 16, type 1 (private identity)
+ buffer.put(new byte[]{(byte) 0b10010000, (byte) 0b00000001});
+ // Identity data
+ buffer.put(CryptorImpIdentityV1.getInstance().encrypt(IDENTITY, SALT, AUTHENTICITY_KEY));
+
+ ByteBuffer deBuffer = ByteBuffer.allocate(28);
+ // Action1 header: length 1, type 6
+ deBuffer.put(new byte[]{(byte) 0b00010110});
+ // Action1 data
+ deBuffer.put((byte) PRESENCE_ACTION_1);
+ // Action2 header: length 1, type 6
+ deBuffer.put(new byte[]{(byte) 0b00010110});
+ // Action2 data
+ deBuffer.put((byte) PRESENCE_ACTION_2);
+ // Ble address header: length 7, type 102
+ deBuffer.put(new byte[]{(byte) 0b10000111, (byte) 0b01100101});
+ // Ble address data
+ deBuffer.put(BLE_ADDRESS);
+ // model id header: length 13, type 7
+ deBuffer.put(new byte[]{(byte) 0b10001101, (byte) 0b00000111});
+ // model id data
+ deBuffer.put(MODE_ID_DATA);
+
+ byte[] data = deBuffer.array();
+ CryptorImpV1 cryptor = CryptorImpV1.getInstance();
+ buffer.put(cryptor.encrypt(data, SALT, AUTHENTICITY_KEY));
+ buffer.put(cryptor.sign(data, AUTHENTICITY_KEY));
+
+ return buffer.array();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementUtilsTest.java
new file mode 100644
index 0000000..c4fccf7
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementUtilsTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+/**
+ * Unit test for {@link ExtendedAdvertisementUtils}.
+ */
+public class ExtendedAdvertisementUtilsTest {
+ private static final byte[] ADVERTISEMENT1 = new byte[]{0b00100000, 12, 34, 78, 10};
+ private static final byte[] ADVERTISEMENT2 = new byte[]{0b00100000, 0b00100011, 34, 78,
+ (byte) 0b10010000, (byte) 0b00000100,
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+
+ private static final int DATA_TYPE_SALT = 3;
+ private static final int DATA_TYPE_PRIVATE_IDENTITY = 4;
+
+ @Test
+ public void test_constructHeader() {
+ assertThat(ExtendedAdvertisementUtils.constructHeader(1)).isEqualTo(0b100000);
+ assertThat(ExtendedAdvertisementUtils.constructHeader(0)).isEqualTo(0);
+ assertThat(ExtendedAdvertisementUtils.constructHeader(6)).isEqualTo((byte) 0b11000000);
+ }
+
+ @Test
+ public void test_getVersion() {
+ assertThat(ExtendedAdvertisementUtils.getVersion(ADVERTISEMENT1)).isEqualTo(1);
+ byte[] adv = new byte[]{(byte) 0b10111100, 9, 19, 90, 23};
+ assertThat(ExtendedAdvertisementUtils.getVersion(adv)).isEqualTo(5);
+ byte[] adv2 = new byte[]{(byte) 0b10011111, 9, 19, 90, 23};
+ assertThat(ExtendedAdvertisementUtils.getVersion(adv2)).isEqualTo(4);
+ }
+
+ @Test
+ public void test_getDataElementHeader_salt() {
+ byte[] saltHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(ADVERTISEMENT2, 1);
+ DataElementHeader header = DataElementHeader.fromBytes(
+ BroadcastRequest.PRESENCE_VERSION_V1, saltHeaderArray);
+ assertThat(header.getDataType()).isEqualTo(DATA_TYPE_SALT);
+ assertThat(header.getDataLength()).isEqualTo(ExtendedAdvertisement.SALT_DATA_LENGTH);
+ }
+
+ @Test
+ public void test_getDataElementHeader_identity() {
+ byte[] identityHeaderArray =
+ ExtendedAdvertisementUtils.getDataElementHeader(ADVERTISEMENT2, 4);
+ DataElementHeader header = DataElementHeader.fromBytes(BroadcastRequest.PRESENCE_VERSION_V1,
+ identityHeaderArray);
+ assertThat(header.getDataType()).isEqualTo(DATA_TYPE_PRIVATE_IDENTITY);
+ assertThat(header.getDataLength()).isEqualTo(ExtendedAdvertisement.IDENTITY_DATA_LENGTH);
+ }
+
+ @Test
+ public void test_constructDataElement_salt() {
+ DataElement salt = new DataElement(DATA_TYPE_SALT, new byte[]{13, 14});
+ byte[] saltArray = ExtendedAdvertisementUtils.convertDataElementToBytes(salt);
+ // Data length and salt header length.
+ assertThat(saltArray.length).isEqualTo(ExtendedAdvertisement.SALT_DATA_LENGTH + 1);
+ // Header
+ assertThat(saltArray[0]).isEqualTo((byte) 0b00100011);
+ // Data
+ assertThat(saltArray[1]).isEqualTo((byte) 13);
+ assertThat(saltArray[2]).isEqualTo((byte) 14);
+ }
+
+ @Test
+ public void test_constructDataElement_privateIdentity() {
+ byte[] identityData = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+ DataElement identity = new DataElement(DATA_TYPE_PRIVATE_IDENTITY, identityData);
+ byte[] identityArray = ExtendedAdvertisementUtils.convertDataElementToBytes(identity);
+ // Data length and identity header length.
+ assertThat(identityArray.length).isEqualTo(ExtendedAdvertisement.IDENTITY_DATA_LENGTH + 2);
+ // 1st header byte
+ assertThat(identityArray[0]).isEqualTo((byte) 0b10010000);
+ // 2st header byte
+ assertThat(identityArray[1]).isEqualTo((byte) 0b00000100);
+ // Data
+ assertThat(Arrays.copyOfRange(identityArray, 2, identityArray.length))
+ .isEqualTo(identityData);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
index 5e0ccbe..8e3e068 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
@@ -75,6 +75,15 @@
assertThat(originalAdvertisement.getVersion()).isEqualTo(
BroadcastRequest.PRESENCE_VERSION_V0);
assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
+ assertThat(originalAdvertisement.getTxPower()).isEqualTo(TX_POWER);
+ assertThat(originalAdvertisement.toString())
+ .isEqualTo("FastAdvertisement:<VERSION: 0, length: 19,"
+ + " ltvFieldCount: 4,"
+ + " identityType: 1,"
+ + " identity: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],"
+ + " salt: [2, 3],"
+ + " actions: [123],"
+ + " txPower: 4");
}
@Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
index 39cab94..856c1a8 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
@@ -18,6 +18,8 @@
import static com.google.common.truth.Truth.assertThat;
+import android.nearby.DataElement;
+import android.nearby.NearbyDeviceParcelable;
import android.nearby.PresenceCredential;
import android.nearby.PresenceDevice;
import android.nearby.PresenceScanFilter;
@@ -28,12 +30,15 @@
import org.junit.Before;
import org.junit.Test;
-import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
/**
* Unit tests for {@link PresenceDiscoveryResult}.
*/
public class PresenceDiscoveryResultTest {
+ private static final int DATA_TYPE_ACCOUNT_KEY = 9;
+ private static final int DATA_TYPE_INTENT = 6;
private static final int PRESENCE_ACTION = 123;
private static final int TX_POWER = -1;
private static final int RSSI = -41;
@@ -43,6 +48,8 @@
private static final byte[] PUBLIC_KEY = new byte[]{1, 1, 2, 2};
private static final byte[] ENCRYPTED_METADATA = new byte[]{1, 2, 3, 4, 5};
private static final byte[] METADATA_ENCRYPTION_KEY_TAG = new byte[]{1, 1, 3, 4, 5};
+ private static final byte[] META_DATA_ENCRYPTION_KEY =
+ new byte[] {-39, -55, 115, 78, -57, 40, 115, 0, -112, 86, -86, 7, -42, 68, 11, 12};
private PresenceDiscoveryResult.Builder mBuilder;
private PublicCredential mCredential;
@@ -59,18 +66,68 @@
.setSalt(SALT)
.setTxPower(TX_POWER)
.setRssi(RSSI)
+ .setEncryptedIdentityTag(METADATA_ENCRYPTION_KEY_TAG)
.addPresenceAction(PRESENCE_ACTION);
}
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void testToDevice() {
- PresenceDiscoveryResult discoveryResult = mBuilder.build();
- PresenceDevice presenceDevice = discoveryResult.toPresenceDevice();
+ public void testFromDevice() {
+ NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder();
+ builder.setTxPower(TX_POWER)
+ .setRssi(RSSI)
+ .setEncryptionKeyTag(METADATA_ENCRYPTION_KEY_TAG)
+ .setSalt(SALT)
+ .setPublicCredential(mCredential);
- assertThat(presenceDevice.getRssi()).isEqualTo(RSSI);
- assertThat(Arrays.equals(presenceDevice.getSalt(), SALT)).isTrue();
- assertThat(Arrays.equals(presenceDevice.getSecretId(), SECRET_ID)).isTrue();
+ PresenceDiscoveryResult discoveryResult =
+ PresenceDiscoveryResult.fromDevice(builder.build());
+ PresenceScanFilter scanFilter = new PresenceScanFilter.Builder()
+ .setMaxPathLoss(80)
+ .addCredential(mCredential)
+ .build();
+
+ assertThat(discoveryResult.matches(scanFilter)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testFromDevice_presenceDeviceAvailable() {
+ NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder();
+ PresenceDevice presenceDevice =
+ new PresenceDevice.Builder("123", SALT, SECRET_ID, META_DATA_ENCRYPTION_KEY)
+ .addExtendedProperty(new DataElement(
+ DATA_TYPE_INTENT, new byte[]{(byte) PRESENCE_ACTION}))
+ .build();
+ builder.setTxPower(TX_POWER)
+ .setRssi(RSSI)
+ .setEncryptionKeyTag(METADATA_ENCRYPTION_KEY_TAG)
+ .setPresenceDevice(presenceDevice)
+ .setPublicCredential(mCredential);
+
+ PresenceDiscoveryResult discoveryResult =
+ PresenceDiscoveryResult.fromDevice(builder.build());
+ PresenceScanFilter scanFilter = new PresenceScanFilter.Builder()
+ .setMaxPathLoss(80)
+ .addPresenceAction(PRESENCE_ACTION)
+ .addCredential(mCredential)
+ .build();
+
+ assertThat(discoveryResult.matches(scanFilter)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testAccountMatches() {
+ DataElement accountKey = new DataElement(DATA_TYPE_ACCOUNT_KEY, new byte[]{1, 2, 3, 4});
+ mBuilder.addExtendedProperties(List.of(accountKey));
+ PresenceDiscoveryResult discoveryResult = mBuilder.build();
+
+ List<DataElement> extendedProperties = new ArrayList<>();
+ extendedProperties.add(new DataElement(DATA_TYPE_ACCOUNT_KEY, new byte[]{1, 2, 3, 4}));
+ extendedProperties.add(new DataElement(DATA_TYPE_INTENT,
+ new byte[]{(byte) PRESENCE_ACTION}));
+ assertThat(discoveryResult.accountKeyMatches(extendedProperties)).isTrue();
}
@Test
@@ -86,4 +143,24 @@
assertThat(discoveryResult.matches(scanFilter)).isTrue();
}
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_notMatches() {
+ PresenceDiscoveryResult.Builder builder = new PresenceDiscoveryResult.Builder()
+ .setPublicCredential(mCredential)
+ .setSalt(SALT)
+ .setTxPower(TX_POWER)
+ .setRssi(RSSI)
+ .setEncryptedIdentityTag(new byte[]{5, 4, 3, 2, 1})
+ .addPresenceAction(PRESENCE_ACTION);
+
+ PresenceScanFilter scanFilter = new PresenceScanFilter.Builder()
+ .setMaxPathLoss(80)
+ .addPresenceAction(PRESENCE_ACTION)
+ .addCredential(mCredential)
+ .build();
+
+ PresenceDiscoveryResult discoveryResult = builder.build();
+ assertThat(discoveryResult.matches(scanFilter)).isFalse();
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
new file mode 100644
index 0000000..ca4f077
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.presence;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.PresenceDevice;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class PresenceManagerTest {
+ private static final byte[] IDENTITY =
+ new byte[] {1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4};
+ private static final byte[] SALT = {2, 3};
+ private static final byte[] SECRET_ID =
+ new byte[] {-97, 10, 107, -86, 25, 65, -54, -95, -72, 59, 54, 93, 9, 3, -24, -88};
+
+ @Mock private Context mContext;
+ private PresenceManager mPresenceManager;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mPresenceManager = new PresenceManager(mContext);
+ when(mContext.getContentResolver())
+ .thenReturn(InstrumentationRegistry.getInstrumentation()
+ .getContext().getContentResolver());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testInit() {
+ mPresenceManager.initiate();
+
+ verify(mContext, times(1)).registerReceiver(any(), any());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testDeviceStatusUpdated() {
+ DataElement dataElement1 = new DataElement(1, new byte[] {1, 2});
+ DataElement dataElement2 = new DataElement(2, new byte[] {-1, -2, 3, 4, 5, 6, 7, 8, 9});
+
+ PresenceDevice presenceDevice =
+ new PresenceDevice.Builder(/* deviceId= */ "deviceId", SALT, SECRET_ID, IDENTITY)
+ .addExtendedProperty(dataElement1)
+ .addExtendedProperty(dataElement2)
+ .build();
+
+ mPresenceManager.mScanCallback.onDiscovered(presenceDevice);
+ mPresenceManager.mScanCallback.onUpdated(presenceDevice);
+ mPresenceManager.mScanCallback.onLost(presenceDevice);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
index d06a785..05b556b 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
@@ -16,6 +16,7 @@
package com.android.server.nearby.provider;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -24,12 +25,14 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.AdvertisingSetCallback;
import android.content.Context;
+import android.hardware.location.ContextHubManager;
import android.nearby.BroadcastCallback;
+import android.nearby.BroadcastRequest;
import androidx.test.core.app.ApplicationProvider;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
import com.android.server.nearby.injector.Injector;
import com.google.common.util.concurrent.MoreExecutors;
@@ -59,9 +62,10 @@
}
@Test
- public void testOnStatus_success() {
+ public void testOnStatus_success_fastAdv() {
byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
- mBleBroadcastProvider.start(advertiseBytes, mBroadcastListener);
+ mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V0,
+ advertiseBytes, mBroadcastListener);
AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
mBleBroadcastProvider.onStartSuccess(settings);
@@ -69,15 +73,47 @@
}
@Test
- public void testOnStatus_failure() {
+ public void testOnStatus_success_extendedAdv() {
byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
- mBleBroadcastProvider.start(advertiseBytes, mBroadcastListener);
+ mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V1,
+ advertiseBytes, mBroadcastListener);
+
+ // advertising set can not be mocked, so we will allow nulls
+ mBleBroadcastProvider.mAdvertisingSetCallback.onAdvertisingSetStarted(null, -30,
+ AdvertisingSetCallback.ADVERTISE_SUCCESS);
+ verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+ }
+
+ @Test
+ public void testOnStatus_failure_fastAdv() {
+ byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
+ mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V0,
+ advertiseBytes, mBroadcastListener);
mBleBroadcastProvider.onStartFailure(BroadcastCallback.STATUS_FAILURE);
verify(mBroadcastListener, times(1))
.onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
}
+ @Test
+ public void testOnStatus_failure_extendedAdv() {
+ byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
+ mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V1,
+ advertiseBytes, mBroadcastListener);
+
+ // advertising set can not be mocked, so we will allow nulls
+ mBleBroadcastProvider.mAdvertisingSetCallback.onAdvertisingSetStarted(null, -30,
+ AdvertisingSetCallback.ADVERTISE_FAILED_INTERNAL_ERROR);
+ // Can be additional failure if the test device does not support LE Extended Advertising.
+ verify(mBroadcastListener, atLeastOnce())
+ .onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
+ }
+
+ @Test
+ public void testStop() {
+ mBleBroadcastProvider.stop();
+ }
+
private static class TestInjector implements Injector {
@Override
@@ -88,7 +124,7 @@
}
@Override
- public ContextHubManagerAdapter getContextHubManagerAdapter() {
+ public ContextHubManager getContextHubManager() {
return null;
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
index 902cc33..2d8bd63 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
@@ -17,6 +17,9 @@
package com.android.server.nearby.provider;
import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
+import static android.nearby.ScanCallback.ERROR_UNKNOWN;
+
+import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
@@ -30,10 +33,13 @@
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.content.Context;
+import android.hardware.location.ContextHubManager;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanFilter;
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
import com.android.server.nearby.injector.Injector;
import org.junit.Before;
@@ -42,6 +48,8 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
public final class BleDiscoveryProviderTest {
@@ -49,6 +57,8 @@
private BleDiscoveryProvider mBleDiscoveryProvider;
@Mock
private AbstractDiscoveryProvider.Listener mListener;
+// @Mock
+// private BluetoothAdapter mBluetoothAdapter;
@Before
public void setup() {
@@ -61,7 +71,7 @@
}
@Test
- public void test_callback() throws InterruptedException {
+ public void test_callback_found() throws InterruptedException {
mBleDiscoveryProvider.getController().setListener(mListener);
mBleDiscoveryProvider.onStart();
mBleDiscoveryProvider.getScanCallback()
@@ -73,11 +83,39 @@
}
@Test
+ public void test_callback_failed() throws InterruptedException {
+ mBleDiscoveryProvider.getController().setListener(mListener);
+ mBleDiscoveryProvider.onStart();
+ mBleDiscoveryProvider.getScanCallback().onScanFailed(1);
+
+
+ // Wait for callback to be invoked
+ Thread.sleep(500);
+ verify(mListener, times(1)).onError(ERROR_UNKNOWN);
+ }
+
+ @Test
public void test_stopScan() {
mBleDiscoveryProvider.onStart();
mBleDiscoveryProvider.onStop();
}
+ @Test
+ public void test_stopScan_filersReset() {
+ List<ScanFilter> filterList = new ArrayList<>();
+ filterList.add(getSanFilter());
+
+ mBleDiscoveryProvider.getController().setProviderScanFilters(filterList);
+ mBleDiscoveryProvider.onStart();
+ mBleDiscoveryProvider.onStop();
+ assertThat(mBleDiscoveryProvider.getFiltersLocked()).isNull();
+ }
+
+ @Test
+ public void testInvalidateScanMode() {
+ mBleDiscoveryProvider.invalidateScanMode();
+ }
+
private class TestInjector implements Injector {
@Override
public BluetoothAdapter getBluetoothAdapter() {
@@ -85,7 +123,7 @@
}
@Override
- public ContextHubManagerAdapter getContextHubManagerAdapter() {
+ public ContextHubManager getContextHubManager() {
return null;
}
@@ -125,4 +163,22 @@
return null;
}
}
+
+ private static PresenceScanFilter getSanFilter() {
+ return new PresenceScanFilter.Builder()
+ .setMaxPathLoss(70)
+ .addCredential(getPublicCredential())
+ .addPresenceAction(124)
+ .build();
+ }
+
+ private static PublicCredential getPublicCredential() {
+ return new PublicCredential.Builder(
+ new byte[]{1, 2},
+ new byte[]{1, 2},
+ new byte[]{1, 2},
+ new byte[]{1, 2},
+ new byte[]{1, 2})
+ .build();
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
index 1b29b52..ce479c8 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
@@ -16,18 +16,33 @@
package com.android.server.nearby.provider;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_MAINLINE_NANO_APP_MIN_VERSION;
+import static com.android.server.nearby.provider.ChreCommunication.INVALID_NANO_APP_VERSION;
+
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.Context;
import android.hardware.location.ContextHubClient;
import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubManager;
import android.hardware.location.ContextHubTransaction;
import android.hardware.location.NanoAppMessage;
import android.hardware.location.NanoAppState;
+import android.os.Build;
+import android.provider.DeviceConfig;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.nearby.NearbyConfiguration;
import com.android.server.nearby.injector.Injector;
import org.junit.Before;
@@ -42,8 +57,12 @@
import java.util.concurrent.Executor;
public class ChreCommunicationTest {
+ private static final String NAMESPACE = NearbyConfiguration.getNamespace();
+ private static final int APP_VERSION = 1;
+
@Mock Injector mInjector;
- @Mock ContextHubManagerAdapter mManager;
+ @Mock Context mContext;
+ @Mock ContextHubManager mManager;
@Mock ContextHubTransaction<List<NanoAppState>> mTransaction;
@Mock ContextHubTransaction.Response<List<NanoAppState>> mTransactionResponse;
@Mock ContextHubClient mClient;
@@ -56,38 +75,78 @@
@Before
public void setUp() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+ DeviceConfig.setProperty(
+ NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION, "1", false);
+
MockitoAnnotations.initMocks(this);
- when(mInjector.getContextHubManagerAdapter()).thenReturn(mManager);
+ when(mInjector.getContextHubManager()).thenReturn(mManager);
when(mManager.getContextHubs()).thenReturn(Collections.singletonList(new ContextHubInfo()));
when(mManager.queryNanoApps(any())).thenReturn(mTransaction);
- when(mManager.createClient(any(), any(), any())).thenReturn(mClient);
+ when(mManager.createClient(any(), any(), any(), any())).thenReturn(mClient);
when(mTransactionResponse.getResult()).thenReturn(ContextHubTransaction.RESULT_SUCCESS);
when(mTransactionResponse.getContents())
.thenReturn(
Collections.singletonList(
- new NanoAppState(ChreDiscoveryProvider.NANOAPP_ID, 1, true)));
+ new NanoAppState(
+ ChreDiscoveryProvider.NANOAPP_ID,
+ APP_VERSION,
+ true)));
- mChreCommunication = new ChreCommunication(mInjector, new InlineExecutor());
+ mChreCommunication = new ChreCommunication(mInjector, mContext, new InlineExecutor());
+ }
+
+ @Test
+ public void testStart() {
mChreCommunication.start(
mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
- }
-
- @Test
- public void testStart() {
verify(mChreCallback).started(true);
}
@Test
public void testStop() {
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+
+ verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+ mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
mChreCommunication.stop();
verify(mClient).close();
}
@Test
+ public void testNotReachMinVersion() {
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION, "3", false);
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+ verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+ mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
+ verify(mChreCallback).started(false);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void test_getNanoVersion() {
+ assertThat(mChreCommunication.queryNanoAppVersion()).isEqualTo(INVALID_NANO_APP_VERSION);
+
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+ verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+ mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
+
+ assertThat(mChreCommunication.queryNanoAppVersion()).isEqualTo(APP_VERSION);
+ }
+
+ @Test
public void testSendMessageToNanApp() {
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+ verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+ mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
NanoAppMessage message =
NanoAppMessage.createMessageToNanoApp(
ChreDiscoveryProvider.NANOAPP_ID,
@@ -99,6 +158,8 @@
@Test
public void testOnMessageFromNanoApp() {
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
NanoAppMessage message =
NanoAppMessage.createMessageToNanoApp(
ChreDiscoveryProvider.NANOAPP_ID,
@@ -109,13 +170,60 @@
}
@Test
+ public void testContextHubTransactionResultToString() {
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_SUCCESS))
+ .isEqualTo("RESULT_SUCCESS");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_UNKNOWN))
+ .isEqualTo("RESULT_FAILED_UNKNOWN");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_BAD_PARAMS))
+ .isEqualTo("RESULT_FAILED_BAD_PARAMS");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_UNINITIALIZED))
+ .isEqualTo("RESULT_FAILED_UNINITIALIZED");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_BUSY))
+ .isEqualTo("RESULT_FAILED_BUSY");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_AT_HUB))
+ .isEqualTo("RESULT_FAILED_AT_HUB");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_TIMEOUT))
+ .isEqualTo("RESULT_FAILED_TIMEOUT");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE))
+ .isEqualTo("RESULT_FAILED_SERVICE_INTERNAL_FAILURE");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_HAL_UNAVAILABLE))
+ .isEqualTo("RESULT_FAILED_HAL_UNAVAILABLE");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(9))
+ .isEqualTo("UNKNOWN_RESULT value=9");
+ }
+
+ @Test
public void testOnHubReset() {
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
mChreCommunication.onHubReset(mClient);
verify(mChreCallback).onHubReset();
}
@Test
public void testOnNanoAppLoaded() {
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
mChreCommunication.onNanoAppLoaded(mClient, ChreDiscoveryProvider.NANOAPP_ID);
verify(mChreCallback).onNanoAppRestart(eq(ChreDiscoveryProvider.NANOAPP_ID));
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
index 7c0dd92..154441b 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
@@ -16,16 +16,34 @@
package com.android.server.nearby.provider;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
+
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.content.Context;
import android.hardware.location.NanoAppMessage;
-import android.nearby.ScanFilter;
+import android.nearby.DataElement;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.OffloadCapability;
+import android.nearby.aidl.IOffloadCallback;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.provider.DeviceConfig;
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.server.nearby.NearbyConfiguration;
+import com.android.server.nearby.presence.PresenceDiscoveryResult;
+
import com.google.protobuf.ByteString;
import org.junit.Before;
@@ -35,22 +53,39 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import service.proto.Blefilter;
-
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
+import service.proto.Blefilter;
+
public class ChreDiscoveryProviderTest {
@Mock AbstractDiscoveryProvider.Listener mListener;
@Mock ChreCommunication mChreCommunication;
+ @Mock IBinder mIBinder;
@Captor ArgumentCaptor<ChreCommunication.ContextHubCommsCallback> mChreCallbackCaptor;
+ @Captor ArgumentCaptor<NearbyDeviceParcelable> mNearbyDevice;
+ private static final String NAMESPACE = NearbyConfiguration.getNamespace();
+ private static final int DATA_TYPE_CONNECTION_STATUS_KEY = 10;
+ private static final int DATA_TYPE_BATTERY_KEY = 11;
+ private static final int DATA_TYPE_TX_POWER_KEY = 5;
+ private static final int DATA_TYPE_BLUETOOTH_ADDR_KEY = 101;
+ private static final int DATA_TYPE_FP_ACCOUNT_KEY = 9;
+ private static final int DATA_TYPE_BLE_SERVICE_DATA_KEY = 100;
+ private static final int DATA_TYPE_TEST_DE_BEGIN_KEY = 2147483520;
+ private static final int DATA_TYPE_TEST_DE_END_KEY = 2147483647;
+
+ private final Object mLock = new Object();
private ChreDiscoveryProvider mChreDiscoveryProvider;
+ private OffloadCapability mOffloadCapability;
@Before
public void setUp() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+
MockitoAnnotations.initMocks(this);
Context context = InstrumentationRegistry.getInstrumentation().getContext();
mChreDiscoveryProvider =
@@ -59,13 +94,68 @@
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void testOnStart() {
- List<ScanFilter> scanFilters = new ArrayList<>();
- mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
- mChreDiscoveryProvider.onStart();
+ public void testInit() {
+ mChreDiscoveryProvider.init();
verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
mChreCallbackCaptor.getValue().started(true);
- verify(mChreCommunication).sendMessageToNanoApp(any());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void test_queryInvalidVersion() {
+ when(mChreCommunication.queryNanoAppVersion()).thenReturn(
+ (long) ChreCommunication.INVALID_NANO_APP_VERSION);
+ IOffloadCallback callback = new IOffloadCallback() {
+ @Override
+ public void onQueryComplete(OffloadCapability capability) throws RemoteException {
+ synchronized (mLock) {
+ mOffloadCapability = capability;
+ mLock.notify();
+ }
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return mIBinder;
+ }
+ };
+ mChreDiscoveryProvider.queryOffloadCapability(callback);
+ OffloadCapability capability =
+ new OffloadCapability
+ .Builder()
+ .setFastPairSupported(false)
+ .setVersion(ChreCommunication.INVALID_NANO_APP_VERSION)
+ .build();
+ assertThat(mOffloadCapability).isEqualTo(capability);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void test_queryVersion() {
+ long version = 500L;
+ when(mChreCommunication.queryNanoAppVersion()).thenReturn(version);
+ IOffloadCallback callback = new IOffloadCallback() {
+ @Override
+ public void onQueryComplete(OffloadCapability capability) throws RemoteException {
+ synchronized (mLock) {
+ mOffloadCapability = capability;
+ mLock.notify();
+ }
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return mIBinder;
+ }
+ };
+ mChreDiscoveryProvider.queryOffloadCapability(callback);
+ OffloadCapability capability =
+ new OffloadCapability
+ .Builder()
+ .setFastPairSupported(true)
+ .setVersion(version)
+ .build();
+ assertThat(mOffloadCapability).isEqualTo(capability);
}
@Test
@@ -93,12 +183,220 @@
ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
results.toByteArray());
mChreDiscoveryProvider.getController().setListener(mListener);
+ mChreDiscoveryProvider.init();
mChreDiscoveryProvider.onStart();
verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
mChreCallbackCaptor.getValue().onMessageFromNanoApp(chre_message);
verify(mListener).onNearbyDeviceDiscovered(any());
}
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testOnNearbyDeviceDiscoveredWithDataElements() {
+ // Disables the setting of test app support
+ boolean isSupportedTestApp = getDeviceConfigBoolean(
+ NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
+ if (isSupportedTestApp) {
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "false", false);
+ }
+ assertThat(new NearbyConfiguration().isTestAppSupported()).isFalse();
+
+ final byte [] connectionStatus = new byte[] {1, 2, 3};
+ final byte [] batteryStatus = new byte[] {4, 5, 6};
+ final byte [] txPower = new byte[] {2};
+ final byte [] bluetoothAddr = new byte[] {1, 2, 3, 4, 5, 6};
+ final byte [] fastPairAccountKey = new byte[16];
+ // First byte is length of service data, padding zeros should be thrown away.
+ final byte [] bleServiceData = new byte[] {5, 1, 2, 3, 4, 5, 0, 0, 0, 0};
+ final byte [] testData = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+ final List<DataElement> expectedExtendedProperties = new ArrayList<>();
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_CONNECTION_STATUS_KEY,
+ connectionStatus));
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_BATTERY_KEY, batteryStatus));
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_TX_POWER_KEY, txPower));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_BLUETOOTH_ADDR_KEY, bluetoothAddr));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_FP_ACCOUNT_KEY, fastPairAccountKey));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_BLE_SERVICE_DATA_KEY, new byte[] {1, 2, 3, 4, 5}));
+
+ Blefilter.PublicCredential credential =
+ Blefilter.PublicCredential.newBuilder()
+ .setSecretId(ByteString.copyFrom(new byte[] {1}))
+ .setAuthenticityKey(ByteString.copyFrom(new byte[2]))
+ .setPublicKey(ByteString.copyFrom(new byte[3]))
+ .setEncryptedMetadata(ByteString.copyFrom(new byte[4]))
+ .setEncryptedMetadataTag(ByteString.copyFrom(new byte[5]))
+ .build();
+ Blefilter.BleFilterResult result =
+ Blefilter.BleFilterResult.newBuilder()
+ .setTxPower(2)
+ .setRssi(1)
+ .setBluetoothAddress(ByteString.copyFrom(bluetoothAddr))
+ .setBleServiceData(ByteString.copyFrom(bleServiceData))
+ .setPublicCredential(credential)
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_CONNECTION_STATUS_KEY)
+ .setValue(ByteString.copyFrom(connectionStatus))
+ .setValueLength(connectionStatus.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_BATTERY_KEY)
+ .setValue(ByteString.copyFrom(batteryStatus))
+ .setValueLength(batteryStatus.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_FP_ACCOUNT_KEY)
+ .setValue(ByteString.copyFrom(fastPairAccountKey))
+ .setValueLength(fastPairAccountKey.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_TEST_DE_BEGIN_KEY)
+ .setValue(ByteString.copyFrom(testData))
+ .setValueLength(testData.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_TEST_DE_END_KEY)
+ .setValue(ByteString.copyFrom(testData))
+ .setValueLength(testData.length)
+ )
+ .build();
+ Blefilter.BleFilterResults results =
+ Blefilter.BleFilterResults.newBuilder().addResult(result).build();
+ NanoAppMessage chre_message =
+ NanoAppMessage.createMessageToNanoApp(
+ ChreDiscoveryProvider.NANOAPP_ID,
+ ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
+ results.toByteArray());
+ mChreDiscoveryProvider.getController().setListener(mListener);
+ mChreDiscoveryProvider.init();
+ mChreDiscoveryProvider.onStart();
+ verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
+ mChreCallbackCaptor.getValue().onMessageFromNanoApp(chre_message);
+ verify(mListener).onNearbyDeviceDiscovered(mNearbyDevice.capture());
+
+ List<DataElement> extendedProperties = PresenceDiscoveryResult
+ .fromDevice(mNearbyDevice.getValue()).getExtendedProperties();
+ assertThat(extendedProperties).containsExactlyElementsIn(expectedExtendedProperties);
+ // Reverts the setting of test app support
+ if (isSupportedTestApp) {
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
+ assertThat(new NearbyConfiguration().isTestAppSupported()).isTrue();
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testOnNearbyDeviceDiscoveredWithTestDataElements() {
+ // Enables the setting of test app support
+ boolean isSupportedTestApp = getDeviceConfigBoolean(
+ NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
+ if (!isSupportedTestApp) {
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
+ }
+ assertThat(new NearbyConfiguration().isTestAppSupported()).isTrue();
+
+ final byte [] connectionStatus = new byte[] {1, 2, 3};
+ final byte [] batteryStatus = new byte[] {4, 5, 6};
+ final byte [] txPower = new byte[] {2};
+ final byte [] bluetoothAddr = new byte[] {1, 2, 3, 4, 5, 6};
+ final byte [] fastPairAccountKey = new byte[16];
+ // First byte is length of service data, padding zeros should be thrown away.
+ final byte [] bleServiceData = new byte[] {5, 1, 2, 3, 4, 5, 0, 0, 0, 0};
+ final byte [] testData = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+ final List<DataElement> expectedExtendedProperties = new ArrayList<>();
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_CONNECTION_STATUS_KEY,
+ connectionStatus));
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_BATTERY_KEY, batteryStatus));
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_TX_POWER_KEY, txPower));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_BLUETOOTH_ADDR_KEY, bluetoothAddr));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_FP_ACCOUNT_KEY, fastPairAccountKey));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_BLE_SERVICE_DATA_KEY, new byte[] {1, 2, 3, 4, 5}));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_TEST_DE_BEGIN_KEY, testData));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_TEST_DE_END_KEY, testData));
+
+ Blefilter.PublicCredential credential =
+ Blefilter.PublicCredential.newBuilder()
+ .setSecretId(ByteString.copyFrom(new byte[] {1}))
+ .setAuthenticityKey(ByteString.copyFrom(new byte[2]))
+ .setPublicKey(ByteString.copyFrom(new byte[3]))
+ .setEncryptedMetadata(ByteString.copyFrom(new byte[4]))
+ .setEncryptedMetadataTag(ByteString.copyFrom(new byte[5]))
+ .build();
+ Blefilter.BleFilterResult result =
+ Blefilter.BleFilterResult.newBuilder()
+ .setTxPower(2)
+ .setRssi(1)
+ .setBluetoothAddress(ByteString.copyFrom(bluetoothAddr))
+ .setBleServiceData(ByteString.copyFrom(bleServiceData))
+ .setPublicCredential(credential)
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_CONNECTION_STATUS_KEY)
+ .setValue(ByteString.copyFrom(connectionStatus))
+ .setValueLength(connectionStatus.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_BATTERY_KEY)
+ .setValue(ByteString.copyFrom(batteryStatus))
+ .setValueLength(batteryStatus.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_FP_ACCOUNT_KEY)
+ .setValue(ByteString.copyFrom(fastPairAccountKey))
+ .setValueLength(fastPairAccountKey.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_TEST_DE_BEGIN_KEY)
+ .setValue(ByteString.copyFrom(testData))
+ .setValueLength(testData.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_TEST_DE_END_KEY)
+ .setValue(ByteString.copyFrom(testData))
+ .setValueLength(testData.length)
+ )
+ .build();
+ Blefilter.BleFilterResults results =
+ Blefilter.BleFilterResults.newBuilder().addResult(result).build();
+ NanoAppMessage chre_message =
+ NanoAppMessage.createMessageToNanoApp(
+ ChreDiscoveryProvider.NANOAPP_ID,
+ ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
+ results.toByteArray());
+ mChreDiscoveryProvider.getController().setListener(mListener);
+ mChreDiscoveryProvider.init();
+ mChreDiscoveryProvider.onStart();
+ verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
+ mChreCallbackCaptor.getValue().onMessageFromNanoApp(chre_message);
+ verify(mListener).onNearbyDeviceDiscovered(mNearbyDevice.capture());
+
+ List<DataElement> extendedProperties = PresenceDiscoveryResult
+ .fromDevice(mNearbyDevice.getValue()).getExtendedProperties();
+ assertThat(extendedProperties).containsExactlyElementsIn(expectedExtendedProperties);
+ // Reverts the setting of test app support
+ if (!isSupportedTestApp) {
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "false", false);
+ assertThat(new NearbyConfiguration().isTestAppSupported()).isFalse();
+ }
+ }
+
+ private boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
+ final String value = getDeviceConfigProperty(name);
+ return value != null ? Boolean.parseBoolean(value) : defaultValue;
+ }
+
+ private String getDeviceConfigProperty(String name) {
+ return DeviceConfig.getProperty(NAMESPACE, name);
+ }
+
private static class InLineExecutor implements Executor {
@Override
public void execute(Runnable command) {
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
new file mode 100644
index 0000000..a759baf
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+
+public final class ArrayUtilsTest {
+
+ private static final byte[] BYTES_ONE = new byte[] {7, 9};
+ private static final byte[] BYTES_TWO = new byte[] {8};
+ private static final byte[] BYTES_EMPTY = new byte[] {};
+ private static final byte[] BYTES_ALL = new byte[] {7, 9, 8};
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysNoInput() {
+ assertThat(ArrayUtils.concatByteArrays().length).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysOneEmptyArray() {
+ assertThat(ArrayUtils.concatByteArrays(BYTES_EMPTY).length).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysOneNonEmptyArray() {
+ assertThat(ArrayUtils.concatByteArrays(BYTES_ONE)).isEqualTo(BYTES_ONE);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysMultipleNonEmptyArrays() {
+ assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_TWO)).isEqualTo(BYTES_ALL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysMultipleArrays() {
+ assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_EMPTY, BYTES_TWO))
+ .isEqualTo(BYTES_ALL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIsEmptyNull_returnsTrue() {
+ assertThat(ArrayUtils.isEmpty(null)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIsEmpty_returnsTrue() {
+ assertThat(ArrayUtils.isEmpty(new byte[]{})).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIsEmpty_returnsFalse() {
+ assertThat(ArrayUtils.isEmpty(BYTES_ALL)).isFalse();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java
index 1a22412..71ade2a 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java
@@ -93,4 +93,9 @@
assertThat(BroadcastPermissions.getPermissionLevel(mMockContext, UID, PID))
.isEqualTo(PERMISSION_BLUETOOTH_ADVERTISE);
}
+
+ @Test
+ public void test_enforceBroadcastPermission() {
+ BroadcastPermissions.enforceBroadcastPermission(mMockContext, mCallerIdentity);
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
new file mode 100644
index 0000000..f0294fc
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.util.Log;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+public class CryptorImpIdentityV1Test {
+ private static final String TAG = "CryptorImpIdentityV1Test";
+ private static final byte[] SALT = new byte[] {102, 22};
+ private static final byte[] DATA =
+ new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+ private static final byte[] AUTHENTICITY_KEY =
+ new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+ @Test
+ public void test_encrypt_decrypt() {
+ Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
+ byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
+
+ assertThat(identityCryptor.decrypt(encryptedData, SALT, AUTHENTICITY_KEY)).isEqualTo(DATA);
+ }
+
+ @Test
+ public void test_encryption() {
+ Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
+ byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
+
+ // for debugging
+ Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
+
+ assertThat(encryptedData).isEqualTo(getEncryptedData());
+ }
+
+ @Test
+ public void test_decryption() {
+ Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
+ byte[] decryptedData =
+ identityCryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
+ // for debugging
+ Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
+
+ assertThat(decryptedData).isEqualTo(DATA);
+ }
+
+ @Test
+ public void generateHmacTag() {
+ CryptorImpIdentityV1 identityCryptor = CryptorImpIdentityV1.getInstance();
+ byte[] generatedTag = identityCryptor.sign(DATA);
+ byte[] expectedTag = new byte[]{50, 116, 95, -87, 63, 123, -79, -43};
+ assertThat(generatedTag).isEqualTo(expectedTag);
+ }
+
+ private static byte[] getEncryptedData() {
+ return new byte[]{6, -31, -32, -123, 43, -92, -47, -110, -65, 126, -15, -51, -19, -43};
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
new file mode 100644
index 0000000..3ca2575
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.util.Log;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+/**
+ * Unit test for {@link CryptorImpV1}
+ */
+public final class CryptorImpV1Test {
+ private static final String TAG = "CryptorImpV1Test";
+ private static final byte[] SALT = new byte[] {102, 22};
+ private static final byte[] DATA =
+ new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+ private static final byte[] AUTHENTICITY_KEY =
+ new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+ @Test
+ public void test_encryption() {
+ Cryptor v1Cryptor = CryptorImpV1.getInstance();
+ byte[] encryptedData = v1Cryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
+
+ // for debugging
+ Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
+
+ assertThat(encryptedData).isEqualTo(getEncryptedData());
+ }
+
+ @Test
+ public void test_encryption_invalidInput() {
+ Cryptor v1Cryptor = CryptorImpV1.getInstance();
+ assertThat(v1Cryptor.encrypt(DATA, SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
+ }
+
+ @Test
+ public void test_decryption() {
+ Cryptor v1Cryptor = CryptorImpV1.getInstance();
+ byte[] decryptedData =
+ v1Cryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
+ // for debugging
+ Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
+
+ assertThat(decryptedData).isEqualTo(DATA);
+ }
+
+ @Test
+ public void test_decryption_invalidInput() {
+ Cryptor v1Cryptor = CryptorImpV1.getInstance();
+ assertThat(v1Cryptor.decrypt(getEncryptedData(), SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
+ }
+
+ @Test
+ public void generateSign() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ byte[] generatedTag = v1Cryptor.sign(DATA, AUTHENTICITY_KEY);
+ byte[] expectedTag = new byte[]{
+ 100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
+ assertThat(generatedTag).isEqualTo(expectedTag);
+ }
+
+ @Test
+ public void test_verify() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ byte[] expectedTag = new byte[]{
+ 100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
+
+ assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, expectedTag)).isTrue();
+ assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, DATA)).isFalse();
+ }
+
+ @Test
+ public void test_generateHmacTag_sameResult() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ byte[] res1 = v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY);
+ assertThat(res1)
+ .isEqualTo(v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY));
+ }
+
+ @Test
+ public void test_generateHmacTag_nullData() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ assertThat(v1Cryptor.generateHmacTag(/* data= */ null, AUTHENTICITY_KEY)).isNull();
+ }
+
+ @Test
+ public void test_generateHmacTag_nullKey() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ assertThat(v1Cryptor.generateHmacTag(DATA, /* authenticityKey= */ null)).isNull();
+ }
+
+ private static byte[] getEncryptedData() {
+ return new byte[]{-92, 94, -99, -97, 81, -48, -7, 119, -64, -22, 45, -49, -50, 92};
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
new file mode 100644
index 0000000..ca612e3
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+/**
+ * Unit test for {@link Cryptor}
+ */
+public final class CryptorTest {
+
+ private static final byte[] DATA =
+ new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+ private static final byte[] AUTHENTICITY_KEY =
+ new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+ @Test
+ public void test_computeHkdf() {
+ int outputSize = 16;
+ byte[] res1 = Cryptor.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize);
+ byte[] res2 = Cryptor.computeHkdf(DATA,
+ new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26},
+ outputSize);
+
+ assertThat(res1).hasLength(outputSize);
+ assertThat(res2).hasLength(outputSize);
+ assertThat(res1).isNotEqualTo(res2);
+ assertThat(res1)
+ .isEqualTo(CryptorImpV1.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize));
+ }
+
+ @Test
+ public void test_computeHkdf_invalidInput() {
+ assertThat(Cryptor.computeHkdf(DATA, AUTHENTICITY_KEY, /* size= */ 256000))
+ .isNull();
+ assertThat(Cryptor.computeHkdf(DATA, new byte[0], /* size= */ 255))
+ .isNull();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java
new file mode 100644
index 0000000..c29cb92
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.nearby.util.identity;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class CallerIdentityTest {
+ private static final int UID = 100;
+ private static final int PID = 10002;
+ private static final String PACKAGE_NAME = "package_name";
+ private static final String ATTRIBUTION_TAG = "attribution_tag";
+
+ @Test
+ public void testToString() {
+ CallerIdentity callerIdentity =
+ CallerIdentity.forTest(UID, PID, PACKAGE_NAME, ATTRIBUTION_TAG);
+ assertThat(callerIdentity.toString()).isEqualTo("100/package_name[attribution_tag]");
+ assertThat(callerIdentity.isSystemServer()).isFalse();
+ }
+
+ @Test
+ public void testHashCode() {
+ CallerIdentity callerIdentity =
+ CallerIdentity.forTest(UID, PID, PACKAGE_NAME, ATTRIBUTION_TAG);
+ CallerIdentity callerIdentity1 =
+ CallerIdentity.forTest(UID, PID, PACKAGE_NAME, ATTRIBUTION_TAG);
+ assertThat(callerIdentity.hashCode()).isEqualTo(callerIdentity1.hashCode());
+ }
+}
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
new file mode 100644
index 0000000..5480ef7
--- /dev/null
+++ b/netbpfload/Android.bp
@@ -0,0 +1,48 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+//
+
+cc_binary {
+ name: "netbpfload",
+
+ defaults: ["bpf_defaults"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wthread-safety",
+ ],
+ sanitize: {
+ integer_overflow: true,
+ },
+
+ header_libs: ["bpf_headers"],
+ shared_libs: [
+ "libbase",
+ "liblog",
+ ],
+ srcs: [
+ "loader.cpp",
+ "NetBpfLoad.cpp",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ "//apex_available:platform",
+ ],
+ // really should be Android 14/U (34), but we cannot include binaries built
+ // against newer sdk in the apex, which still targets 30(R):
+ // module "netbpfload" variant "android_x86_apex30": should support
+ // min_sdk_version(30) for "com.android.tethering": newer SDK(34).
+ min_sdk_version: "30",
+}
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index 8e47ea8..b44a0bc 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -44,12 +44,11 @@
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
#include <android-base/unique_fd.h>
-#include <libbpf_android.h>
#include <log/log.h>
-#include <netdutils/Misc.h>
-#include <netdutils/Slice.h>
+
#include "BpfSyscallWrappers.h"
#include "bpf/BpfUtils.h"
+#include "loader.h"
using android::base::EndsWith;
using android::bpf::domain;
@@ -66,76 +65,34 @@
abort(); // can only hit this if permissions (likely selinux) are screwed up
}
-constexpr unsigned long long kTetheringApexDomainBitmask =
- domainToBitmask(domain::tethering) |
- domainToBitmask(domain::net_private) |
- domainToBitmask(domain::net_shared) |
- domainToBitmask(domain::netd_readonly) |
- domainToBitmask(domain::netd_shared);
-
-// Programs shipped inside the tethering apex should be limited to networking stuff,
-// as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
-// since they are less stable abi/api and may conflict with platform uses of bpf.
-constexpr bpf_prog_type kTetheringApexAllowedProgTypes[] = {
- BPF_PROG_TYPE_CGROUP_SKB,
- BPF_PROG_TYPE_CGROUP_SOCK,
- BPF_PROG_TYPE_CGROUP_SOCKOPT,
- BPF_PROG_TYPE_CGROUP_SOCK_ADDR,
- BPF_PROG_TYPE_CGROUP_SYSCTL,
- BPF_PROG_TYPE_LWT_IN,
- BPF_PROG_TYPE_LWT_OUT,
- BPF_PROG_TYPE_LWT_SEG6LOCAL,
- BPF_PROG_TYPE_LWT_XMIT,
- BPF_PROG_TYPE_SCHED_ACT,
- BPF_PROG_TYPE_SCHED_CLS,
- BPF_PROG_TYPE_SOCKET_FILTER,
- BPF_PROG_TYPE_SOCK_OPS,
- BPF_PROG_TYPE_XDP,
-};
-
const android::bpf::Location locations[] = {
// S+ Tethering mainline module (network_stack): tether offload
{
.dir = "/apex/com.android.tethering/etc/bpf/",
.prefix = "tethering/",
- .allowedDomainBitmask = kTetheringApexDomainBitmask,
- .allowedProgTypes = kTetheringApexAllowedProgTypes,
- .allowedProgTypesLength = arraysize(kTetheringApexAllowedProgTypes),
},
// T+ Tethering mainline module (shared with netd & system server)
// netutils_wrapper (for iptables xt_bpf) has access to programs
{
.dir = "/apex/com.android.tethering/etc/bpf/netd_shared/",
.prefix = "netd_shared/",
- .allowedDomainBitmask = kTetheringApexDomainBitmask,
- .allowedProgTypes = kTetheringApexAllowedProgTypes,
- .allowedProgTypesLength = arraysize(kTetheringApexAllowedProgTypes),
},
// T+ Tethering mainline module (shared with netd & system server)
// netutils_wrapper has no access, netd has read only access
{
.dir = "/apex/com.android.tethering/etc/bpf/netd_readonly/",
.prefix = "netd_readonly/",
- .allowedDomainBitmask = kTetheringApexDomainBitmask,
- .allowedProgTypes = kTetheringApexAllowedProgTypes,
- .allowedProgTypesLength = arraysize(kTetheringApexAllowedProgTypes),
},
// T+ Tethering mainline module (shared with system server)
{
.dir = "/apex/com.android.tethering/etc/bpf/net_shared/",
.prefix = "net_shared/",
- .allowedDomainBitmask = kTetheringApexDomainBitmask,
- .allowedProgTypes = kTetheringApexAllowedProgTypes,
- .allowedProgTypesLength = arraysize(kTetheringApexAllowedProgTypes),
},
// T+ Tethering mainline module (not shared, just network_stack)
{
.dir = "/apex/com.android.tethering/etc/bpf/net_private/",
.prefix = "net_private/",
- .allowedDomainBitmask = kTetheringApexDomainBitmask,
- .allowedProgTypes = kTetheringApexAllowedProgTypes,
- .allowedProgTypesLength = arraysize(kTetheringApexAllowedProgTypes),
},
};
@@ -278,13 +235,6 @@
if (createSysFsBpfSubDir(location.prefix)) return 1;
}
- // Note: there's no actual src dir for fs_bpf_loader .o's,
- // so it is not listed in 'locations[].prefix'.
- // This is because this is primarily meant for triggering genfscon rules,
- // and as such this will likely always be the case.
- // Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
- if (createSysFsBpfSubDir("loader")) return 1;
-
// Load all ELF objects, create programs and maps, and pin them
for (const auto& location : locations) {
if (loadAllElfObjects(location) != 0) {
diff --git a/netbpfload/loader.cpp b/netbpfload/loader.cpp
index 3bb758b..c534b2c 100644
--- a/netbpfload/loader.cpp
+++ b/netbpfload/loader.cpp
@@ -43,7 +43,7 @@
#include "BpfSyscallWrappers.h"
#include "bpf/BpfUtils.h"
#include "bpf/bpf_map_def.h"
-#include "include/libbpf_android.h"
+#include "loader.h"
#if BPFLOADER_VERSION < COMPILE_FOR_BPFLOADER_VERSION
#error "BPFLOADER_VERSION is less than COMPILE_FOR_BPFLOADER_VERSION"
@@ -59,9 +59,9 @@
#include <android-base/cmsg.h>
#include <android-base/file.h>
+#include <android-base/properties.h>
#include <android-base/strings.h>
#include <android-base/unique_fd.h>
-#include <cutils/properties.h>
#define BPF_FS_PATH "/sys/fs/bpf/"
@@ -79,17 +79,11 @@
using std::string;
using std::vector;
-static std::string getBuildTypeInternal() {
- char value[PROPERTY_VALUE_MAX] = {};
- (void)property_get("ro.build.type", value, "unknown"); // ignore length
- return value;
-}
-
namespace android {
namespace bpf {
const std::string& getBuildType() {
- static std::string t = getBuildTypeInternal();
+ static std::string t = android::base::GetProperty("ro.build.type", "unknown");
return t;
}
@@ -178,6 +172,10 @@
*
* However, be aware that you should not be directly using the SECTION() macro.
* Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE/CRITICAL macros.
+ *
+ * Programs shipped inside the tethering apex should be limited to networking stuff,
+ * as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
+ * since they are less stable abi/api and may conflict with platform uses of bpf.
*/
sectionType sectionNameTypes[] = {
{"bind4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND},
@@ -189,13 +187,10 @@
{"egress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_EGRESS},
{"getsockopt/", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_GETSOCKOPT},
{"ingress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_INGRESS},
- {"kprobe/", BPF_PROG_TYPE_KPROBE, BPF_ATTACH_TYPE_UNSPEC},
- {"kretprobe/", BPF_PROG_TYPE_KPROBE, BPF_ATTACH_TYPE_UNSPEC},
{"lwt_in/", BPF_PROG_TYPE_LWT_IN, BPF_ATTACH_TYPE_UNSPEC},
{"lwt_out/", BPF_PROG_TYPE_LWT_OUT, BPF_ATTACH_TYPE_UNSPEC},
{"lwt_seg6local/", BPF_PROG_TYPE_LWT_SEG6LOCAL, BPF_ATTACH_TYPE_UNSPEC},
{"lwt_xmit/", BPF_PROG_TYPE_LWT_XMIT, BPF_ATTACH_TYPE_UNSPEC},
- {"perf_event/", BPF_PROG_TYPE_PERF_EVENT, BPF_ATTACH_TYPE_UNSPEC},
{"postbind4/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET4_POST_BIND},
{"postbind6/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET6_POST_BIND},
{"recvmsg4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_RECVMSG},
@@ -208,9 +203,6 @@
{"skfilter/", BPF_PROG_TYPE_SOCKET_FILTER, BPF_ATTACH_TYPE_UNSPEC},
{"sockops/", BPF_PROG_TYPE_SOCK_OPS, BPF_CGROUP_SOCK_OPS},
{"sysctl", BPF_PROG_TYPE_CGROUP_SYSCTL, BPF_CGROUP_SYSCTL},
- {"tracepoint/", BPF_PROG_TYPE_TRACEPOINT, BPF_ATTACH_TYPE_UNSPEC},
- {"uprobe/", BPF_PROG_TYPE_KPROBE, BPF_ATTACH_TYPE_UNSPEC},
- {"uretprobe/", BPF_PROG_TYPE_KPROBE, BPF_ATTACH_TYPE_UNSPEC},
{"xdp/", BPF_PROG_TYPE_XDP, BPF_ATTACH_TYPE_UNSPEC},
};
@@ -393,19 +385,10 @@
return 0;
}
-static enum bpf_prog_type getFuseProgType() {
- int result = BPF_PROG_TYPE_UNSPEC;
- ifstream("/sys/fs/fuse/bpf_prog_type_fuse") >> result;
- return static_cast<bpf_prog_type>(result);
-}
-
static enum bpf_prog_type getSectionType(string& name) {
for (auto& snt : sectionNameTypes)
if (StartsWith(name, snt.name)) return snt.type;
- // TODO Remove this code when fuse-bpf is upstream and this BPF_PROG_TYPE_FUSE is fixed
- if (StartsWith(name, "fuse/")) return getFuseProgType();
-
return BPF_PROG_TYPE_UNSPEC;
}
@@ -415,6 +398,7 @@
return BPF_ATTACH_TYPE_UNSPEC;
}
+/*
static string getSectionName(enum bpf_prog_type type)
{
for (auto& snt : sectionNameTypes)
@@ -423,6 +407,7 @@
return "UNKNOWN SECTION NAME " + std::to_string(type);
}
+*/
static int readProgDefs(ifstream& elfFile, vector<struct bpf_prog_def>& pd,
size_t sizeOfBpfProgDef) {
@@ -502,22 +487,8 @@
return 0;
}
-static bool IsAllowed(bpf_prog_type type, const bpf_prog_type* allowed, size_t numAllowed) {
- if (allowed == nullptr) return true;
-
- for (size_t i = 0; i < numAllowed; i++) {
- if (allowed[i] == BPF_PROG_TYPE_UNSPEC) {
- if (type == getFuseProgType()) return true;
- } else if (type == allowed[i])
- return true;
- }
-
- return false;
-}
-
/* Read a section by its index - for ex to get sec hdr strtab blob */
-static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs, size_t sizeOfBpfProgDef,
- const bpf_prog_type* allowed, size_t numAllowed) {
+static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs, size_t sizeOfBpfProgDef) {
vector<Elf64_Shdr> shTable;
int entries, ret = 0;
@@ -544,11 +515,6 @@
if (ptype == BPF_PROG_TYPE_UNSPEC) continue;
- if (!IsAllowed(ptype, allowed, numAllowed)) {
- ALOGE("Program type %s not permitted here", getSectionName(ptype).c_str());
- return -1;
- }
-
// This must be done before '/' is replaced with '_'.
cs_temp.expected_attach_type = getExpectedAttachType(name);
@@ -655,8 +621,7 @@
}
static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
- const char* prefix, const unsigned long long allowedDomainBitmask,
- const size_t sizeOfBpfMapDef) {
+ const char* prefix, const size_t sizeOfBpfMapDef) {
int ret;
vector<char> mdData;
vector<struct bpf_map_def> md;
@@ -767,11 +732,6 @@
domain selinux_context = getDomainFromSelinuxContext(md[i].selinux_context);
if (specified(selinux_context)) {
- if (!inDomainBitmask(selinux_context, allowedDomainBitmask)) {
- ALOGE("map %s has invalid selinux_context of %d (allowed bitmask 0x%llx)",
- mapNames[i].c_str(), selinux_context, allowedDomainBitmask);
- return -EINVAL;
- }
ALOGI("map %s selinux_context [%-32s] -> %d -> '%s' (%s)", mapNames[i].c_str(),
md[i].selinux_context, selinux_context, lookupSelinuxContext(selinux_context),
lookupPinSubdir(selinux_context));
@@ -780,11 +740,6 @@
domain pin_subdir = getDomainFromPinSubdir(md[i].pin_subdir);
if (unrecognized(pin_subdir)) return -ENOTDIR;
if (specified(pin_subdir)) {
- if (!inDomainBitmask(pin_subdir, allowedDomainBitmask)) {
- ALOGE("map %s has invalid pin_subdir of %d (allowed bitmask 0x%llx)",
- mapNames[i].c_str(), pin_subdir, allowedDomainBitmask);
- return -EINVAL;
- }
ALOGI("map %s pin_subdir [%-32s] -> %d -> '%s'", mapNames[i].c_str(), md[i].pin_subdir,
pin_subdir, lookupPinSubdir(pin_subdir));
}
@@ -955,7 +910,7 @@
}
static int loadCodeSections(const char* elfPath, vector<codeSection>& cs, const string& license,
- const char* prefix, const unsigned long long allowedDomainBitmask) {
+ const char* prefix) {
unsigned kvers = kernelVersion();
if (!kvers) {
@@ -1014,22 +969,12 @@
if (unrecognized(pin_subdir)) return -ENOTDIR;
if (specified(selinux_context)) {
- if (!inDomainBitmask(selinux_context, allowedDomainBitmask)) {
- ALOGE("prog %s has invalid selinux_context of %d (allowed bitmask 0x%llx)",
- name.c_str(), selinux_context, allowedDomainBitmask);
- return -EINVAL;
- }
ALOGI("prog %s selinux_context [%-32s] -> %d -> '%s' (%s)", name.c_str(),
cs[i].prog_def->selinux_context, selinux_context,
lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
}
if (specified(pin_subdir)) {
- if (!inDomainBitmask(pin_subdir, allowedDomainBitmask)) {
- ALOGE("prog %s has invalid pin_subdir of %d (allowed bitmask 0x%llx)", name.c_str(),
- pin_subdir, allowedDomainBitmask);
- return -EINVAL;
- }
ALOGI("prog %s pin_subdir [%-32s] -> %d -> '%s'", name.c_str(),
cs[i].prog_def->pin_subdir, pin_subdir, lookupPinSubdir(pin_subdir));
}
@@ -1210,8 +1155,7 @@
return -1;
}
- ret = readCodeSections(elfFile, cs, sizeOfBpfProgDef, location.allowedProgTypes,
- location.allowedProgTypesLength);
+ ret = readCodeSections(elfFile, cs, sizeOfBpfProgDef);
if (ret) {
ALOGE("Couldn't read all code sections in %s", elfPath);
return ret;
@@ -1220,8 +1164,7 @@
/* Just for future debugging */
if (0) dumpAllCs(cs);
- ret = createMaps(elfPath, elfFile, mapFds, location.prefix, location.allowedDomainBitmask,
- sizeOfBpfMapDef);
+ ret = createMaps(elfPath, elfFile, mapFds, location.prefix, sizeOfBpfMapDef);
if (ret) {
ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
return ret;
@@ -1232,8 +1175,7 @@
applyMapRelo(elfFile, mapFds, cs);
- ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix,
- location.allowedDomainBitmask);
+ ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix);
if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
return ret;
diff --git a/netbpfload/loader.h b/netbpfload/loader.h
index a47e4da..b884637 100644
--- a/netbpfload/loader.h
+++ b/netbpfload/loader.h
@@ -64,20 +64,9 @@
return d != domain::unspecified;
}
-static constexpr unsigned long long domainToBitmask(domain d) {
- return specified(d) ? 1uLL << (static_cast<int>(d) - 1) : 0;
-}
-
-static constexpr bool inDomainBitmask(domain d, unsigned long long v) {
- return domainToBitmask(d) & v;
-}
-
struct Location {
const char* const dir = "";
const char* const prefix = "";
- unsigned long long allowedDomainBitmask = 0;
- const bpf_prog_type* allowedProgTypes = nullptr;
- size_t allowedProgTypesLength = 0;
};
// BPF loader implementation. Loads an eBPF ELF object
diff --git a/netbpfload/netbpfload.rc b/netbpfload/netbpfload.rc
deleted file mode 100644
index 20fbb9f..0000000
--- a/netbpfload/netbpfload.rc
+++ /dev/null
@@ -1,85 +0,0 @@
-# zygote-start is what officially starts netd (see //system/core/rootdir/init.rc)
-# However, on some hardware it's started from post-fs-data as well, which is just
-# a tad earlier. There's no benefit to that though, since on 4.9+ P+ devices netd
-# will just block until bpfloader finishes and sets the bpf.progs_loaded property.
-#
-# It is important that we start netbpfload after:
-# - /sys/fs/bpf is already mounted,
-# - apex (incl. rollback) is initialized (so that in the future we can load bpf
-# programs shipped as part of apex mainline modules)
-# - logd is ready for us to log stuff
-#
-# At the same time we want to be as early as possible to reduce races and thus
-# failures (before memory is fragmented, and cpu is busy running tons of other
-# stuff) and we absolutely want to be before netd and the system boot slot is
-# considered to have booted successfully.
-#
-on load_bpf_programs
- exec_start netbpfload
-
-service netbpfload /system/bin/netbpfload
- capabilities CHOWN SYS_ADMIN NET_ADMIN
- # The following group memberships are a workaround for lack of DAC_OVERRIDE
- # and allow us to open (among other things) files that we created and are
- # no longer root owned (due to CHOWN) but still have group read access to
- # one of the following groups. This is not perfect, but a more correct
- # solution requires significantly more effort to implement.
- group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
- user root
- #
- # Set RLIMIT_MEMLOCK to 1GiB for netbpfload
- #
- # Actually only 8MiB would be needed if netbpfload ran as its own uid.
- #
- # However, while the rlimit is per-thread, the accounting is system wide.
- # So, for example, if the graphics stack has already allocated 10MiB of
- # memlock data before netbpfload even gets a chance to run, it would fail
- # if its memlock rlimit is only 8MiB - since there would be none left for it.
- #
- # netbpfload succeeding is critical to system health, since a failure will
- # cause netd crashloop and thus system server crashloop... and the only
- # recovery is a full kernel reboot.
- #
- # We've had issues where devices would sometimes (rarely) boot into
- # a crashloop because netbpfload would occasionally lose a boot time
- # race against the graphics stack's boot time locked memory allocation.
- #
- # Thus netbpfload's memlock has to be 8MB higher then the locked memory
- # consumption of the root uid anywhere else in the system...
- # But we don't know what that is for all possible devices...
- #
- # Ideally, we'd simply grant netbpfload the IPC_LOCK capability and it
- # would simply ignore it's memlock rlimit... but it turns that this
- # capability is not even checked by the kernel's bpf system call.
- #
- # As such we simply use 1GiB as a reasonable approximation of infinity.
- #
- rlimit memlock 1073741824 1073741824
- oneshot
- #
- # How to debug bootloops caused by 'netbpfload-failed'.
- #
- # 1. On some lower RAM devices (like wembley) you may need to first enable developer mode
- # (from the Settings app UI), and change the developer option "Logger buffer sizes"
- # from the default (wembley: 64kB) to the maximum (1M) per log buffer.
- # Otherwise buffer will overflow before you manage to dump it and you'll get useless logs.
- #
- # 2. comment out 'reboot_on_failure reboot,netbpfload-failed' below
- # 3. rebuild/reflash/reboot
- # 4. as the device is booting up capture netbpfload logs via:
- # adb logcat -s 'NetBpfLoad:*' 'NetBpfLoader:*'
- #
- # something like:
- # $ adb reboot; sleep 1; adb wait-for-device; adb root; sleep 1; adb wait-for-device; adb logcat -s 'NetBpfLoad:*' 'NetBpfLoader:*'
- # will take care of capturing logs as early as possible
- #
- # 5. look through the logs from the kernel's bpf verifier that netbpfload dumps out,
- # it usually makes sense to search back from the end and find the particular
- # bpf verifier failure that caused netbpfload to terminate early with an error code.
- # This will probably be something along the lines of 'too many jumps' or
- # 'cannot prove return value is 0 or 1' or 'unsupported / unknown operation / helper',
- # 'invalid bpf_context access', etc.
- #
- reboot_on_failure reboot,netbpfload-failed
- # we're not really updatable, but want to be able to load bpf programs shipped in apexes
- updatable
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index fa92f10..a7a4059 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -130,12 +130,21 @@
attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
}
- // This should trivially pass, since we just attached up above,
- // but BPF_PROG_QUERY is only implemented on 4.19+ kernels.
if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ RETURN_IF_NOT_OK(attachProgramToCgroup(
+ "/sys/fs/bpf/netd_readonly/prog_block_bind4_block_port",
+ cg_fd, BPF_CGROUP_INET4_BIND));
+ RETURN_IF_NOT_OK(attachProgramToCgroup(
+ "/sys/fs/bpf/netd_readonly/prog_block_bind6_block_port",
+ cg_fd, BPF_CGROUP_INET6_BIND));
+
+ // This should trivially pass, since we just attached up above,
+ // but BPF_PROG_QUERY is only implemented on 4.19+ kernels.
if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_EGRESS) <= 0) abort();
if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_INGRESS) <= 0) abort();
if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_CREATE) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_BIND) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
}
return netdutils::status::ok;
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index 16a8242..a21c033 100644
--- a/remoteauth/tests/unit/Android.bp
+++ b/remoteauth/tests/unit/Android.bp
@@ -20,7 +20,7 @@
name: "RemoteAuthUnitTests",
defaults: [
"enable-remoteauth-targets",
- "mts-target-sdk-version-current"
+ "mts-target-sdk-version-current",
],
sdk_version: "test_current",
min_sdk_version: "31",
@@ -45,7 +45,7 @@
"mockito-target-extended-minus-junit4",
"platform-test-annotations",
"service-remoteauth-pre-jarjar",
- "truth-prebuilt",
+ "truth",
],
// these are needed for Extended Mockito
jni_libs: [
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 7e2d2f4..bc49f0e 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -102,7 +102,12 @@
],
exclude_srcs: [
"src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java",
- "src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java"
+ "src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java",
+ "src/com/android/server/connectivity/mdns/MdnsAdvertiser.java",
+ "src/com/android/server/connectivity/mdns/MdnsAnnouncer.java",
+ "src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java",
+ "src/com/android/server/connectivity/mdns/MdnsProber.java",
+ "src/com/android/server/connectivity/mdns/MdnsRecordRepository.java",
],
static_libs: [
"net-utils-device-common-mdns-standalone-build-test",
diff --git a/service-t/Sources.bp b/service-t/Sources.bp
index 187eadf..fbe02a5 100644
--- a/service-t/Sources.bp
+++ b/service-t/Sources.bp
@@ -20,7 +20,6 @@
srcs: [
"jni/com_android_server_net_NetworkStatsFactory.cpp",
],
- path: "jni",
visibility: [
"//packages/modules/Connectivity:__subpackages__",
],
@@ -32,7 +31,6 @@
"jni/com_android_server_net_NetworkStatsFactory.cpp",
"jni/com_android_server_net_NetworkStatsService.cpp",
],
- path: "jni",
visibility: [
"//packages/modules/Connectivity:__subpackages__",
],
diff --git a/service-t/lint-baseline.xml b/service-t/lint-baseline.xml
new file mode 100644
index 0000000..38d3ab0
--- /dev/null
+++ b/service-t/lint-baseline.xml
@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier#getInterfaceName`"
+ errorLine1=" if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+ line="224"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getInterface`"
+ errorLine1=" delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="276"
+ column="55"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getOwnerUid`"
+ errorLine1=" delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="276"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getUnderlyingInterfaces`"
+ errorLine1=" info.getUnderlyingInterfaces());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="277"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+ errorLine1=" dnsAddresses.add(InetAddress.parseNumericAddress(address));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
+ line="875"
+ column="54"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+ errorLine1=" staticIpConfigBuilder.setGateway(InetAddress.parseNumericAddress(value));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
+ line="870"
+ column="66"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(os);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsRecorder.java"
+ line="556"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(sockFd);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+ line="1309"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(mSocket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+ line="1034"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
+ errorLine1=" .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java"
+ line="156"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
+ errorLine1=" nc.setNetworkSpecifier(new EthernetNetworkSpecifier(iface));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+ line="218"
+ column="36"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.util.AtomicFile`"
+ errorLine1=" mFile = new AtomicFile(new File(path), logger);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/PersistentInt.java"
+ line="53"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new java.net.InetSocketAddress`"
+ errorLine1=" super(handler, new RecvBuffer(buffer, new InetSocketAddress()));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java"
+ line="66"
+ column="47"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
+ errorLine1=" .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java"
+ line="156"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
+ errorLine1=" nc.setNetworkSpecifier(new EthernetNetworkSpecifier(iface));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+ line="218"
+ column="36"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier`"
+ errorLine1=" if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+ line="224"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier`"
+ errorLine1=" if (!(spec instanceof EthernetNetworkSpecifier)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+ line="221"
+ column="31"/>
+ </issue>
+
+</issues>
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
index d9bc643..5812797 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
@@ -110,7 +110,7 @@
@NonNull MdnsReplySender replySender,
@Nullable PacketRepeaterCallback<BaseAnnouncementInfo> cb,
@NonNull SharedLog sharedLog) {
- super(looper, replySender, cb, sharedLog);
+ super(looper, replySender, cb, sharedLog, MdnsAdvertiser.DBG);
}
// TODO: Notify MdnsRecordRepository that the records were announced for that service ID,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java b/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
index 1251170..b83a6a0 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
@@ -19,6 +19,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import java.net.InetAddress;
+import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
@@ -42,6 +43,10 @@
public static final String SUBTYPE_PREFIX = "_";
private static final String MDNS_IPV4_HOST_ADDRESS = "224.0.0.251";
private static final String MDNS_IPV6_HOST_ADDRESS = "FF02::FB";
+ public static final InetSocketAddress IPV6_SOCKET_ADDR = new InetSocketAddress(
+ getMdnsIPv6Address(), MDNS_PORT);
+ public static final InetSocketAddress IPV4_SOCKET_ADDR = new InetSocketAddress(
+ getMdnsIPv4Address(), MDNS_PORT);
private static InetAddress mdnsAddress;
private MdnsConstants() {
}
@@ -75,4 +80,4 @@
public static Charset getUtf8Charset() {
return UTF_8;
}
-}
\ No newline at end of file
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index 436adec..e07d380 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -161,7 +161,7 @@
@NonNull SharedLog sharedLog) {
return new MdnsReplySender(looper, socket, packetCreationBuffer,
sharedLog.forSubComponent(
- MdnsReplySender.class.getSimpleName() + "/" + interfaceTag));
+ MdnsReplySender.class.getSimpleName() + "/" + interfaceTag), DBG);
}
/** @see MdnsAnnouncer */
@@ -372,7 +372,7 @@
// happen when the incoming packet has answer records (not a question), so there will be no
// answer. One exception is simultaneous probe tiebreaking (rfc6762 8.2), in which case the
// conflicting service is still probing and won't reply either.
- final MdnsRecordRepository.ReplyInfo answers = mRecordRepository.getReply(packet, src);
+ final MdnsReplyInfo answers = mRecordRepository.getReply(packet, src);
if (answers == null) return;
mReplySender.queueReply(answers);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
index 7fa3f84..1fabd49 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
@@ -31,6 +31,7 @@
public class MdnsPacket {
private static final String TAG = MdnsPacket.class.getSimpleName();
+ public final int transactionId;
public final int flags;
@NonNull
public final List<MdnsRecord> questions;
@@ -46,6 +47,15 @@
@NonNull List<MdnsRecord> answers,
@NonNull List<MdnsRecord> authorityRecords,
@NonNull List<MdnsRecord> additionalRecords) {
+ this(0, flags, questions, answers, authorityRecords, additionalRecords);
+ }
+
+ MdnsPacket(int transactionId, int flags,
+ @NonNull List<MdnsRecord> questions,
+ @NonNull List<MdnsRecord> answers,
+ @NonNull List<MdnsRecord> authorityRecords,
+ @NonNull List<MdnsRecord> additionalRecords) {
+ this.transactionId = transactionId;
this.flags = flags;
this.questions = Collections.unmodifiableList(questions);
this.answers = Collections.unmodifiableList(answers);
@@ -70,15 +80,16 @@
*/
@NonNull
public static MdnsPacket parse(@NonNull MdnsPacketReader reader) throws ParseException {
+ final int transactionId;
final int flags;
try {
- reader.readUInt16(); // transaction ID (not used)
+ transactionId = reader.readUInt16();
flags = reader.readUInt16();
} catch (EOFException e) {
throw new ParseException(MdnsResponseErrorCode.ERROR_END_OF_FILE,
"Reached the end of the mDNS response unexpectedly.", e);
}
- return parseRecordsSection(reader, flags);
+ return parseRecordsSection(reader, flags, transactionId);
}
/**
@@ -86,8 +97,8 @@
*
* The records section starts with the questions count, just after the packet flags.
*/
- public static MdnsPacket parseRecordsSection(@NonNull MdnsPacketReader reader, int flags)
- throws ParseException {
+ public static MdnsPacket parseRecordsSection(@NonNull MdnsPacketReader reader, int flags,
+ int transactionId) throws ParseException {
try {
final int numQuestions = reader.readUInt16();
final int numAnswers = reader.readUInt16();
@@ -99,7 +110,7 @@
final ArrayList<MdnsRecord> authority = parseRecords(reader, numAuthority, false);
final ArrayList<MdnsRecord> additional = parseRecords(reader, numAdditional, false);
- return new MdnsPacket(flags, questions, answers, authority, additional);
+ return new MdnsPacket(transactionId, flags, questions, answers, authority, additional);
} catch (EOFException e) {
throw new ParseException(MdnsResponseErrorCode.ERROR_END_OF_FILE,
"Reached the end of the mDNS response unexpectedly.", e);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
index fd0f5c9..e84cead 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
@@ -16,8 +16,8 @@
package com.android.server.connectivity.mdns;
-import static com.android.server.connectivity.mdns.MdnsRecordRepository.IPV4_ADDR;
-import static com.android.server.connectivity.mdns.MdnsRecordRepository.IPV6_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -38,9 +38,8 @@
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public abstract class MdnsPacketRepeater<T extends MdnsPacketRepeater.Request> {
- private static final boolean DBG = MdnsAdvertiser.DBG;
private static final InetSocketAddress[] ALL_ADDRS = new InetSocketAddress[] {
- IPV4_ADDR, IPV6_ADDR
+ IPV4_SOCKET_ADDR, IPV6_SOCKET_ADDR
};
@NonNull
@@ -51,6 +50,7 @@
private final PacketRepeaterCallback<T> mCb;
@NonNull
private final SharedLog mSharedLog;
+ private final boolean mEnableDebugLog;
/**
* Status callback from {@link MdnsPacketRepeater}.
@@ -111,7 +111,7 @@
}
final MdnsPacket packet = request.getPacket(index);
- if (DBG) {
+ if (mEnableDebugLog) {
mSharedLog.v("Sending packets for iteration " + index + " out of "
+ request.getNumSends() + " for ID " + msg.what);
}
@@ -134,7 +134,7 @@
// likely not to be available since the device is in deep sleep anyway.
final long delay = request.getDelayMs(nextIndex);
sendMessageDelayed(obtainMessage(msg.what, nextIndex, 0, request), delay);
- if (DBG) mSharedLog.v("Scheduled next packet in " + delay + "ms");
+ if (mEnableDebugLog) mSharedLog.v("Scheduled next packet in " + delay + "ms");
}
// Call onSent after scheduling the next run, to allow the callback to cancel it
@@ -145,15 +145,17 @@
}
protected MdnsPacketRepeater(@NonNull Looper looper, @NonNull MdnsReplySender replySender,
- @Nullable PacketRepeaterCallback<T> cb, @NonNull SharedLog sharedLog) {
+ @Nullable PacketRepeaterCallback<T> cb, @NonNull SharedLog sharedLog,
+ boolean enableDebugLog) {
mHandler = new ProbeHandler(looper);
mReplySender = replySender;
mCb = cb;
mSharedLog = sharedLog;
+ mEnableDebugLog = enableDebugLog;
}
protected void startSending(int id, @NonNull T request, long initialDelayMs) {
- if (DBG) {
+ if (mEnableDebugLog) {
mSharedLog.v("Starting send with id " + id + ", request "
+ request.getClass().getSimpleName() + ", delay " + initialDelayMs);
}
@@ -172,7 +174,7 @@
// all in the handler queue; unless this method is called from a message, but the current
// message cannot be cancelled.
if (mHandler.hasMessages(id)) {
- if (DBG) {
+ if (mEnableDebugLog) {
mSharedLog.v("Stopping send on id " + id);
}
mHandler.removeMessages(id);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
index f2b562a..e88947a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
@@ -40,9 +40,8 @@
private static final long CONFLICT_RETRY_DELAY_MS = 5_000L;
public MdnsProber(@NonNull Looper looper, @NonNull MdnsReplySender replySender,
- @NonNull PacketRepeaterCallback<ProbingInfo> cb,
- @NonNull SharedLog sharedLog) {
- super(looper, replySender, cb, sharedLog);
+ @NonNull PacketRepeaterCallback<ProbingInfo> cb, @NonNull SharedLog sharedLog) {
+ super(looper, replySender, cb, sharedLog, MdnsAdvertiser.DBG);
}
/** Probing request to send with {@link MdnsProber}. */
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index 063680e..73c1758 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -16,6 +16,8 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
import android.annotation.NonNull;
@@ -79,11 +81,6 @@
private static final String[] DNS_SD_SERVICE_TYPE =
new String[] { "_services", "_dns-sd", "_udp", LOCAL_TLD };
- public static final InetSocketAddress IPV6_ADDR = new InetSocketAddress(
- MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
- public static final InetSocketAddress IPV4_ADDR = new InetSocketAddress(
- MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
-
@NonNull
private final Random mDelayGenerator = new Random();
// Map of service unique ID -> records for service
@@ -468,44 +465,13 @@
}
/**
- * Info about a reply to be sent.
- */
- public static class ReplyInfo {
- @NonNull
- public final List<MdnsRecord> answers;
- @NonNull
- public final List<MdnsRecord> additionalAnswers;
- public final long sendDelayMs;
- @NonNull
- public final InetSocketAddress destination;
-
- public ReplyInfo(
- @NonNull List<MdnsRecord> answers,
- @NonNull List<MdnsRecord> additionalAnswers,
- long sendDelayMs,
- @NonNull InetSocketAddress destination) {
- this.answers = answers;
- this.additionalAnswers = additionalAnswers;
- this.sendDelayMs = sendDelayMs;
- this.destination = destination;
- }
-
- @Override
- public String toString() {
- return "{ReplyInfo to " + destination + ", answers: " + answers.size()
- + ", additionalAnswers: " + additionalAnswers.size()
- + ", sendDelayMs " + sendDelayMs + "}";
- }
- }
-
- /**
* Get the reply to send to an incoming packet.
*
* @param packet The incoming packet.
* @param src The source address of the incoming packet.
*/
@Nullable
- public ReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
+ public MdnsReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
final long now = SystemClock.elapsedRealtime();
final boolean replyUnicast = (packet.flags & MdnsConstants.QCLASS_UNICAST) != 0;
final ArrayList<MdnsRecord> additionalAnswerRecords = new ArrayList<>();
@@ -556,9 +522,9 @@
if (replyUnicast) {
dest = src;
} else if (src.getAddress() instanceof Inet4Address) {
- dest = IPV4_ADDR;
+ dest = IPV4_SOCKET_ADDR;
} else {
- dest = IPV6_ADDR;
+ dest = IPV6_SOCKET_ADDR;
}
// Build the list of answer records from their RecordInfo
@@ -572,7 +538,7 @@
answerRecords.add(info.record);
}
- return new ReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest);
+ return new MdnsReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest);
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
new file mode 100644
index 0000000..ce61b54
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+
+import java.net.InetSocketAddress;
+import java.util.List;
+
+/**
+ * Info about a mDNS reply to be sent.
+ */
+public final class MdnsReplyInfo {
+ @NonNull
+ public final List<MdnsRecord> answers;
+ @NonNull
+ public final List<MdnsRecord> additionalAnswers;
+ public final long sendDelayMs;
+ @NonNull
+ public final InetSocketAddress destination;
+
+ public MdnsReplyInfo(
+ @NonNull List<MdnsRecord> answers,
+ @NonNull List<MdnsRecord> additionalAnswers,
+ long sendDelayMs,
+ @NonNull InetSocketAddress destination) {
+ this.answers = answers;
+ this.additionalAnswers = additionalAnswers;
+ this.sendDelayMs = sendDelayMs;
+ this.destination = destination;
+ }
+
+ @Override
+ public String toString() {
+ return "{MdnsReplyInfo to " + destination + ", answers: " + answers.size()
+ + ", additionalAnswers: " + additionalAnswers.size()
+ + ", sendDelayMs " + sendDelayMs + "}";
+ }
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
index 3d64b5a..abf5d99 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -26,7 +26,6 @@
import android.os.Message;
import com.android.net.module.util.SharedLog;
-import com.android.server.connectivity.mdns.MdnsRecordRepository.ReplyInfo;
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
@@ -45,7 +44,6 @@
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class MdnsReplySender {
- private static final boolean DBG = MdnsAdvertiser.DBG;
private static final int MSG_SEND = 1;
private static final int PACKET_NOT_SENT = 0;
private static final int PACKET_SENT = 1;
@@ -58,24 +56,27 @@
private final byte[] mPacketCreationBuffer;
@NonNull
private final SharedLog mSharedLog;
+ private final boolean mEnableDebugLog;
public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
- @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog) {
+ @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
+ boolean enableDebugLog) {
mHandler = new SendHandler(looper);
mSocket = socket;
mPacketCreationBuffer = packetCreationBuffer;
mSharedLog = sharedLog;
+ mEnableDebugLog = enableDebugLog;
}
/**
* Queue a reply to be sent when its send delay expires.
*/
- public void queueReply(@NonNull ReplyInfo reply) {
+ public void queueReply(@NonNull MdnsReplyInfo reply) {
ensureRunningOnHandlerThread(mHandler);
// TODO: implement response aggregation (RFC 6762 6.4)
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
- if (DBG) {
+ if (mEnableDebugLog) {
mSharedLog.v("Scheduling " + reply);
}
}
@@ -118,8 +119,8 @@
@Override
public void handleMessage(@NonNull Message msg) {
- final ReplyInfo replyInfo = (ReplyInfo) msg.obj;
- if (DBG) mSharedLog.v("Sending " + replyInfo);
+ final MdnsReplyInfo replyInfo = (MdnsReplyInfo) msg.obj;
+ if (mEnableDebugLog) mSharedLog.v("Sending " + replyInfo);
final int flags = 0x8400; // Response, authoritative (rfc6762 18.4)
final MdnsPacket packet = new MdnsPacket(flags,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
index a3cc0eb..050913f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -90,14 +90,14 @@
final MdnsPacket mdnsPacket;
try {
- reader.readUInt16(); // transaction ID (not used)
+ final int transactionId = reader.readUInt16();
int flags = reader.readUInt16();
if ((flags & MdnsConstants.FLAGS_RESPONSE_MASK) != MdnsConstants.FLAGS_RESPONSE) {
throw new MdnsPacket.ParseException(
MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE, "Not a response", null);
}
- mdnsPacket = MdnsPacket.parseRecordsSection(reader, flags);
+ mdnsPacket = MdnsPacket.parseRecordsSection(reader, flags, transactionId);
if (mdnsPacket.answers.size() < 1) {
throw new MdnsPacket.ParseException(
MdnsResponseErrorCode.ERROR_NO_ANSWERS, "Response has no answers",
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
index ec6af9b..f9ee0df 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -42,7 +42,7 @@
* to their default value (0, false or null).
*/
public class MdnsServiceCache {
- private static class CacheKey {
+ static class CacheKey {
@NonNull final String mLowercaseServiceType;
@NonNull final SocketKey mSocketKey;
@@ -72,6 +72,12 @@
*/
@NonNull
private final ArrayMap<CacheKey, List<MdnsResponse>> mCachedServices = new ArrayMap<>();
+ /**
+ * A map of service expire callbacks. Key is composed of service type and socket and value is
+ * the callback listener.
+ */
+ @NonNull
+ private final ArrayMap<CacheKey, ServiceExpiredCallback> mCallbacks = new ArrayMap<>();
@NonNull
private final Handler mHandler;
@@ -82,17 +88,14 @@
/**
* Get the cache services which are queried from given service type and socket.
*
- * @param serviceType the target service type.
- * @param socketKey the target socket
+ * @param cacheKey the target CacheKey.
* @return the set of services which matches the given service type.
*/
@NonNull
- public List<MdnsResponse> getCachedServices(@NonNull String serviceType,
- @NonNull SocketKey socketKey) {
+ public List<MdnsResponse> getCachedServices(@NonNull CacheKey cacheKey) {
ensureRunningOnHandlerThread(mHandler);
- final CacheKey key = new CacheKey(serviceType, socketKey);
- return mCachedServices.containsKey(key)
- ? Collections.unmodifiableList(new ArrayList<>(mCachedServices.get(key)))
+ return mCachedServices.containsKey(cacheKey)
+ ? Collections.unmodifiableList(new ArrayList<>(mCachedServices.get(cacheKey)))
: Collections.emptyList();
}
@@ -117,16 +120,13 @@
* Get the cache service.
*
* @param serviceName the target service name.
- * @param serviceType the target service type.
- * @param socketKey the target socket
+ * @param cacheKey the target CacheKey.
* @return the service which matches given conditions.
*/
@Nullable
- public MdnsResponse getCachedService(@NonNull String serviceName,
- @NonNull String serviceType, @NonNull SocketKey socketKey) {
+ public MdnsResponse getCachedService(@NonNull String serviceName, @NonNull CacheKey cacheKey) {
ensureRunningOnHandlerThread(mHandler);
- final List<MdnsResponse> responses =
- mCachedServices.get(new CacheKey(serviceType, socketKey));
+ final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
if (responses == null) {
return null;
}
@@ -137,15 +137,13 @@
/**
* Add or update a service.
*
- * @param serviceType the service type.
- * @param socketKey the target socket
+ * @param cacheKey the target CacheKey.
* @param response the response of the discovered service.
*/
- public void addOrUpdateService(@NonNull String serviceType, @NonNull SocketKey socketKey,
- @NonNull MdnsResponse response) {
+ public void addOrUpdateService(@NonNull CacheKey cacheKey, @NonNull MdnsResponse response) {
ensureRunningOnHandlerThread(mHandler);
final List<MdnsResponse> responses = mCachedServices.computeIfAbsent(
- new CacheKey(serviceType, socketKey), key -> new ArrayList<>());
+ cacheKey, key -> new ArrayList<>());
// Remove existing service if present.
final MdnsResponse existing =
findMatchedResponse(responses, response.getServiceInstanceName());
@@ -157,15 +155,12 @@
* Remove a service which matches the given service name, type and socket.
*
* @param serviceName the target service name.
- * @param serviceType the target service type.
- * @param socketKey the target socket.
+ * @param cacheKey the target CacheKey.
*/
@Nullable
- public MdnsResponse removeService(@NonNull String serviceName, @NonNull String serviceType,
- @NonNull SocketKey socketKey) {
+ public MdnsResponse removeService(@NonNull String serviceName, @NonNull CacheKey cacheKey) {
ensureRunningOnHandlerThread(mHandler);
- final List<MdnsResponse> responses =
- mCachedServices.get(new CacheKey(serviceType, socketKey));
+ final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
if (responses == null) {
return null;
}
@@ -180,5 +175,37 @@
return null;
}
+ /**
+ * Register a callback to listen to service expiration.
+ *
+ * <p> Registering the same callback instance twice is a no-op, since MdnsServiceTypeClient
+ * relies on this.
+ *
+ * @param cacheKey the target CacheKey.
+ * @param callback the callback that notify the service is expired.
+ */
+ public void registerServiceExpiredCallback(@NonNull CacheKey cacheKey,
+ @NonNull ServiceExpiredCallback callback) {
+ ensureRunningOnHandlerThread(mHandler);
+ mCallbacks.put(cacheKey, callback);
+ }
+
+ /**
+ * Unregister the service expired callback.
+ *
+ * @param cacheKey the CacheKey that is registered to listen service expiration before.
+ */
+ public void unregisterServiceExpiredCallback(@NonNull CacheKey cacheKey) {
+ ensureRunningOnHandlerThread(mHandler);
+ mCallbacks.remove(cacheKey);
+ }
+
+ /*** Callbacks for listening service expiration */
+ public interface ServiceExpiredCallback {
+ /*** Notify the service is expired */
+ void onServiceRecordExpired(@NonNull MdnsResponse previousResponse,
+ @Nullable MdnsResponse newResponse);
+ }
+
// TODO: check ttl expiration for each service and notify to the clients.
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index bbe8f4c..0a03186 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -16,6 +16,7 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsServiceCache.ServiceExpiredCallback;
import static com.android.server.connectivity.mdns.MdnsServiceCache.findMatchedResponse;
import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
@@ -71,6 +72,15 @@
* The service caches for each socket. It should be accessed from looper thread only.
*/
@NonNull private final MdnsServiceCache serviceCache;
+ @NonNull private final MdnsServiceCache.CacheKey cacheKey;
+ @NonNull private final ServiceExpiredCallback serviceExpiredCallback =
+ new ServiceExpiredCallback() {
+ @Override
+ public void onServiceRecordExpired(@NonNull MdnsResponse previousResponse,
+ @Nullable MdnsResponse newResponse) {
+ notifyRemovedServiceToListeners(previousResponse, "Service record expired");
+ }
+ };
private final ArrayMap<MdnsServiceBrowserListener, MdnsSearchOptions> listeners =
new ArrayMap<>();
private final boolean removeServiceAfterTtlExpires =
@@ -225,6 +235,16 @@
this.dependencies = dependencies;
this.serviceCache = serviceCache;
this.mdnsQueryScheduler = new MdnsQueryScheduler();
+ this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey);
+ }
+
+ /**
+ * Do the cleanup of the MdnsServiceTypeClient
+ */
+ private void shutDown() {
+ removeScheduledTask();
+ mdnsQueryScheduler.cancelScheduledRun();
+ serviceCache.unregisterServiceExpiredCallback(cacheKey);
}
private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(
@@ -293,7 +313,7 @@
boolean hadReply = false;
if (listeners.put(listener, searchOptions) == null) {
for (MdnsResponse existingResponse :
- serviceCache.getCachedServices(serviceType, socketKey)) {
+ serviceCache.getCachedServices(cacheKey)) {
if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
final MdnsServiceInfo info =
buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
@@ -341,6 +361,8 @@
servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
executor.submit(queryTask);
}
+
+ serviceCache.registerServiceExpiredCallback(cacheKey, serviceExpiredCallback);
}
/**
@@ -390,8 +412,7 @@
return listeners.isEmpty();
}
if (listeners.isEmpty()) {
- removeScheduledTask();
- mdnsQueryScheduler.cancelScheduledRun();
+ shutDown();
}
return listeners.isEmpty();
}
@@ -404,8 +425,7 @@
ensureRunningOnHandlerThread(handler);
// Augment the list of current known responses, and generated responses for resolve
// requests if there is no known response
- final List<MdnsResponse> cachedList =
- serviceCache.getCachedServices(serviceType, socketKey);
+ final List<MdnsResponse> cachedList = serviceCache.getCachedServices(cacheKey);
final List<MdnsResponse> currentList = new ArrayList<>(cachedList);
List<MdnsResponse> additionalResponses = makeResponsesForResolve(socketKey);
for (MdnsResponse additionalResponse : additionalResponses) {
@@ -432,7 +452,7 @@
} else if (findMatchedResponse(cachedList, serviceInstanceName) != null) {
// If the response is not modified and already in the cache. The cache will
// need to be updated to refresh the last receipt time.
- serviceCache.addOrUpdateService(serviceType, socketKey, response);
+ serviceCache.addOrUpdateService(cacheKey, response);
}
}
if (dependencies.hasMessages(handler, EVENT_START_QUERYTASK)) {
@@ -458,44 +478,50 @@
}
}
- /** Notify all services are removed because the socket is destroyed. */
- public void notifySocketDestroyed() {
- ensureRunningOnHandlerThread(handler);
- for (MdnsResponse response : serviceCache.getCachedServices(serviceType, socketKey)) {
- final String name = response.getServiceInstanceName();
- if (name == null) continue;
- for (int i = 0; i < listeners.size(); i++) {
- if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
- final MdnsServiceBrowserListener listener = listeners.keyAt(i);
- final MdnsServiceInfo serviceInfo =
- buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
+ private void notifyRemovedServiceToListeners(@NonNull MdnsResponse response,
+ @NonNull String message) {
+ for (int i = 0; i < listeners.size(); i++) {
+ if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
+ final MdnsServiceBrowserListener listener = listeners.keyAt(i);
+ if (response.getServiceInstanceName() != null) {
+ final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
+ response, serviceTypeLabels);
if (response.isComplete()) {
- sharedLog.log("Socket destroyed. onServiceRemoved: " + name);
+ sharedLog.log(message + ". onServiceRemoved: " + serviceInfo);
listener.onServiceRemoved(serviceInfo);
}
- sharedLog.log("Socket destroyed. onServiceNameRemoved: " + name);
+ sharedLog.log(message + ". onServiceNameRemoved: " + serviceInfo);
listener.onServiceNameRemoved(serviceInfo);
}
}
- removeScheduledTask();
- mdnsQueryScheduler.cancelScheduledRun();
+ }
+
+ /** Notify all services are removed because the socket is destroyed. */
+ public void notifySocketDestroyed() {
+ ensureRunningOnHandlerThread(handler);
+ for (MdnsResponse response : serviceCache.getCachedServices(cacheKey)) {
+ final String name = response.getServiceInstanceName();
+ if (name == null) continue;
+ notifyRemovedServiceToListeners(response, "Socket destroyed");
+ }
+ shutDown();
}
private void onResponseModified(@NonNull MdnsResponse response) {
final String serviceInstanceName = response.getServiceInstanceName();
final MdnsResponse currentResponse =
- serviceCache.getCachedService(serviceInstanceName, serviceType, socketKey);
+ serviceCache.getCachedService(serviceInstanceName, cacheKey);
boolean newServiceFound = false;
boolean serviceBecomesComplete = false;
if (currentResponse == null) {
newServiceFound = true;
if (serviceInstanceName != null) {
- serviceCache.addOrUpdateService(serviceType, socketKey, response);
+ serviceCache.addOrUpdateService(cacheKey, response);
}
} else {
boolean before = currentResponse.isComplete();
- serviceCache.addOrUpdateService(serviceType, socketKey, response);
+ serviceCache.addOrUpdateService(cacheKey, response);
boolean after = response.isComplete();
serviceBecomesComplete = !before && after;
}
@@ -529,22 +555,11 @@
private void onGoodbyeReceived(@Nullable String serviceInstanceName) {
final MdnsResponse response =
- serviceCache.removeService(serviceInstanceName, serviceType, socketKey);
+ serviceCache.removeService(serviceInstanceName, cacheKey);
if (response == null) {
return;
}
- for (int i = 0; i < listeners.size(); i++) {
- if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
- final MdnsServiceBrowserListener listener = listeners.keyAt(i);
- final MdnsServiceInfo serviceInfo =
- buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
- if (response.isComplete()) {
- sharedLog.log("onServiceRemoved: " + serviceInfo);
- listener.onServiceRemoved(serviceInfo);
- }
- sharedLog.log("onServiceNameRemoved: " + serviceInfo);
- listener.onServiceNameRemoved(serviceInfo);
- }
+ notifyRemovedServiceToListeners(response, "Goodbye received");
}
private boolean shouldRemoveServiceAfterTtlExpires() {
@@ -567,7 +582,7 @@
continue;
}
MdnsResponse knownResponse =
- serviceCache.getCachedService(resolveName, serviceType, socketKey);
+ serviceCache.getCachedService(resolveName, cacheKey);
if (knownResponse == null) {
final ArrayList<String> instanceFullName = new ArrayList<>(
serviceTypeLabels.length + 1);
@@ -585,36 +600,18 @@
private void tryRemoveServiceAfterTtlExpires() {
if (!shouldRemoveServiceAfterTtlExpires()) return;
- Iterator<MdnsResponse> iter =
- serviceCache.getCachedServices(serviceType, socketKey).iterator();
+ final Iterator<MdnsResponse> iter = serviceCache.getCachedServices(cacheKey).iterator();
while (iter.hasNext()) {
MdnsResponse existingResponse = iter.next();
- final String serviceInstanceName = existingResponse.getServiceInstanceName();
if (existingResponse.hasServiceRecord()
&& existingResponse.getServiceRecord()
.getRemainingTTL(clock.elapsedRealtime()) == 0) {
- serviceCache.removeService(serviceInstanceName, serviceType, socketKey);
- for (int i = 0; i < listeners.size(); i++) {
- if (!responseMatchesOptions(existingResponse, listeners.valueAt(i))) {
- continue;
- }
- final MdnsServiceBrowserListener listener = listeners.keyAt(i);
- if (serviceInstanceName != null) {
- final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
- existingResponse, serviceTypeLabels);
- if (existingResponse.isComplete()) {
- sharedLog.log("TTL expired. onServiceRemoved: " + serviceInfo);
- listener.onServiceRemoved(serviceInfo);
- }
- sharedLog.log("TTL expired. onServiceNameRemoved: " + serviceInfo);
- listener.onServiceNameRemoved(serviceInfo);
- }
- }
+ serviceCache.removeService(existingResponse.getServiceInstanceName(), cacheKey);
+ notifyRemovedServiceToListeners(existingResponse, "TTL expired");
}
}
}
-
private static class QuerySentArguments {
private final int transactionId;
private final List<String> subTypes = new ArrayList<>();
@@ -672,7 +669,7 @@
private long getMinRemainingTtl(long now) {
long minRemainingTtl = Long.MAX_VALUE;
- for (MdnsResponse response : serviceCache.getCachedServices(serviceType, socketKey)) {
+ for (MdnsResponse response : serviceCache.getCachedServices(cacheKey)) {
if (!response.isComplete()) {
continue;
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index 0dcc560..d0f3d9a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -189,7 +189,7 @@
// TODO: support packets over size (send in multiple packets with TC bit set)
final MdnsPacketWriter writer = new MdnsPacketWriter(packetCreationBuffer);
- writer.writeUInt16(0); // Transaction ID (advertisement: 0)
+ writer.writeUInt16(packet.transactionId); // Transaction ID (advertisement: 0)
writer.writeUInt16(packet.flags); // Response, authoritative (rfc6762 18.4)
writer.writeUInt16(packet.questions.size()); // questions count
writer.writeUInt16(packet.answers.size()); // answers count
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 48e86d8..01b8de7 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -48,6 +48,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
@@ -237,7 +238,18 @@
mDeps = deps;
// Interface match regex.
- mIfaceMatch = mDeps.getInterfaceRegexFromResource(mContext);
+ String ifaceMatchRegex = mDeps.getInterfaceRegexFromResource(mContext);
+ // "*" is a magic string to indicate "pick the default".
+ if (ifaceMatchRegex.equals("*")) {
+ if (SdkLevel.isAtLeastU()) {
+ // On U+, include both usb%d and eth%d interfaces.
+ ifaceMatchRegex = "(usb|eth)\\d+";
+ } else {
+ // On T, include only eth%d interfaces.
+ ifaceMatchRegex = "eth\\d+";
+ }
+ }
+ mIfaceMatch = ifaceMatchRegex;
// Read default Ethernet interface configuration from resources
final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context);
diff --git a/service/ServiceConnectivityResources/res/values-as/strings.xml b/service/ServiceConnectivityResources/res/values-as/strings.xml
index e753cb3..7e4dd42 100644
--- a/service/ServiceConnectivityResources/res/values-as/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-as/strings.xml
@@ -18,7 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"ছিষ্টেম সংযোগৰ উৎস"</string>
- <string name="wifi_available_sign_in" msgid="8041178343789805553">"ৱাই-ফাই নেটৱৰ্কত ছাইন ইন কৰক"</string>
+ <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi নেটৱৰ্কত ছাইন ইন কৰক"</string>
<string name="network_available_sign_in" msgid="2622520134876355561">"নেটৱৰ্কত ছাইন ইন কৰক"</string>
<!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
<skip />
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index f30abc6..045d707f 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -194,8 +194,11 @@
-->
</string-array>
- <!-- Regex of wired ethernet ifaces -->
- <string translatable="false" name="config_ethernet_iface_regex">eth\\d</string>
+ <!-- Regex of wired ethernet ifaces. Network interfaces that match this regex will be tracked
+ by ethernet service.
+ If set to "*", ethernet service uses "(eth|usb)\\d+" on Android U+ and eth\\d+ on
+ Android T. -->
+ <string translatable="false" name="config_ethernet_iface_regex">*</string>
<!-- Ignores Wi-Fi validation failures after roam.
If validation fails on a Wi-Fi network after a roam to a new BSSID,
diff --git a/service/lint-baseline.xml b/service/lint-baseline.xml
new file mode 100644
index 0000000..5149e6d
--- /dev/null
+++ b/service/lint-baseline.xml
@@ -0,0 +1,510 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.app.PendingIntent#intentFilterEquals`"
+ errorLine1=" return a.intentFilterEquals(b);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1358"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.app.usage.NetworkStatsManager#notifyNetworkStatus`"
+ errorLine1=" mStatsManager.notifyNetworkStatus(getDefaultNetworks(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="9938"
+ column="27"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.content.pm.ApplicationInfo#isOem`"
+ errorLine1=" return appInfo.isVendor() || appInfo.isOem() || appInfo.isProduct();"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="481"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.content.pm.ApplicationInfo#isProduct`"
+ errorLine1=" return appInfo.isVendor() || appInfo.isOem() || appInfo.isProduct();"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="481"
+ column="65"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.content.pm.ApplicationInfo#isVendor`"
+ errorLine1=" return appInfo.isVendor() || appInfo.isOem() || appInfo.isProduct();"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="481"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getMultipathPreference`"
+ errorLine1=" networkPreference = netPolicyManager.getMultipathPreference(network);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="5498"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getRestrictBackgroundStatus`"
+ errorLine1=" return mPolicyManager.getRestrictBackgroundStatus(callerUid);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2565"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidNetworkingBlocked`"
+ errorLine1=" return mPolicyManager.isUidNetworkingBlocked(uid, metered);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1914"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidRestrictedOnMeteredNetworks`"
+ errorLine1=" if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="7094"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#registerNetworkPolicyCallback`"
+ errorLine1=" mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1567"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getLinkProperties`"
+ errorLine1=" snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2584"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetworkCapabilities`"
+ errorLine1=" snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2584"
+ column="64"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
+ errorLine1=" snapshot.getNetwork(), snapshot.getSubscriberId()));"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2585"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
+ errorLine1=" final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(snapshot.getNetwork());"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2581"
+ column="81"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getSubscriberId`"
+ errorLine1=" snapshot.getNetwork(), snapshot.getSubscriberId()));"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2585"
+ column="57"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager#getWatchlistConfigHash`"
+ errorLine1=" return nwm.getWatchlistConfigHash();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="10060"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#addPacProxyInstalledListener`"
+ errorLine1=" mPacProxyManager.addPacProxyInstalledListener("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="111"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
+ errorLine1=" () -> mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="208"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
+ errorLine1=" mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="252"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportMobileRadioPowerState`"
+ errorLine1=" bs.reportMobileRadioPowerState(isActive, NO_UID);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="11006"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportNetworkInterfaceForTransports`"
+ errorLine1=" batteryStats.reportNetworkInterfaceForTransports(iface, transportTypes);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1347"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportWifiRadioPowerState`"
+ errorLine1=" bs.reportWifiRadioPowerState(isActive, NO_UID);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="11009"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+ errorLine1=" if (Build.isDebuggable()) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="9074"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+ errorLine1=" if (!Build.isDebuggable()) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="5039"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
+ errorLine1=" for (final int uid : mSystemConfigManager.getSystemPermissionUids(INTERNET)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="396"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
+ errorLine1=" for (final int uid : mSystemConfigManager.getSystemPermissionUids(UPDATE_DEVICE_STATS)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="404"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+ errorLine1=" final int uid = handle.getUid(appId);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="1069"
+ column="44"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.tos = Os.getsockoptInt(fd, IPPROTO_IP, IP_TOS);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="285"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.ttl = Os.getsockoptInt(fd, IPPROTO_IP, IP_TTL);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="287"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.ack = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="265"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.seq = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="262"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
+ errorLine1=" final int result = Os.ioctlInt(fd, SIOCINQ);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="392"
+ column="31"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
+ errorLine1=" final int result = Os.ioctlInt(fd, SIOCOUTQ);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="402"
+ column="31"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+ errorLine1=' InetAddress.parseNumericAddress("::").getAddress();'
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyValue.java"
+ line="99"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+ errorLine1=' private static final InetAddress GOOGLE_DNS_4 = InetAddress.parseNumericAddress("8.8.8.8");'
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ClatCoordinator.java"
+ line="89"
+ column="65"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(pfd);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="9991"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(pfd);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="10008"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(mFileDescriptor);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkDiagnostics.java"
+ line="481"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.NetworkStateSnapshot`"
+ errorLine1=" return new NetworkStateSnapshot(network, new NetworkCapabilities(networkCapabilities),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkAgentInfo.java"
+ line="1269"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.UnderlyingNetworkInfo`"
+ errorLine1=" return new UnderlyingNetworkInfo(nai.networkCapabilities.getOwnerUid(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="6123"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager.NetworkPolicyCallback`"
+ errorLine1=" private final NetworkPolicyCallback mPolicyCallback = new NetworkPolicyCallback() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2827"
+ column="63"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
+ errorLine1=" mContext.getSystemService(NetworkPolicyManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="5493"
+ column="44"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
+ errorLine1=" mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1554"
+ column="52"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager`"
+ errorLine1=" NetworkWatchlistManager nwm = mContext.getSystemService(NetworkWatchlistManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="10054"
+ column="65"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.PacProxyManager.PacProxyInstalledListener`"
+ errorLine1=" private class PacProxyInstalledListener implements PacProxyManager.PacProxyInstalledListener {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="90"
+ column="56"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.PacProxyManager`"
+ errorLine1=" mPacProxyManager = context.getSystemService(PacProxyManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="108"
+ column="53"/>
+ </issue>
+
+</issues>
\ No newline at end of file
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index b5dbf96..8475110 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -97,17 +97,20 @@
import static android.system.OsConstants.ETH_P_ALL;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
+
import static com.android.net.module.util.NetworkMonitorUtils.isPrivateDnsValidationRequired;
import static com.android.net.module.util.PermissionUtils.checkAnyPermissionOf;
import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr;
import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
+
import static java.util.Map.Entry;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.app.ActivityManager.UidFrozenStateChangedCallback;
@@ -1314,6 +1317,10 @@
return SdkLevel.isAtLeastU();
}
+ public boolean isAtLeastV() {
+ return SdkLevel.isAtLeastV();
+ }
+
/**
* Get system properties to use in ConnectivityService.
*/
@@ -3480,6 +3487,8 @@
sendStickyBroadcast(makeGeneralIntent(info, bcastType));
}
+ // TODO(b/193460475): Remove when tooling supports SystemApi to public API.
+ @SuppressLint("NewApi")
// TODO: Set the mini sdk to 31 and remove @TargetApi annotation when b/205923322 is addressed.
@TargetApi(Build.VERSION_CODES.S)
private void sendStickyBroadcast(Intent intent) {
@@ -8025,6 +8034,7 @@
}
}
}
+ if (!highestPriorityNri.isBeingSatisfied()) return null;
return highestPriorityNri.getSatisfier();
}
@@ -9135,6 +9145,8 @@
// else not handled
}
+ // TODO(b/193460475): Remove when tooling supports SystemApi to public API.
+ @SuppressLint("NewApi")
private void sendIntent(PendingIntent pendingIntent, Intent intent) {
mPendingIntentWakeLock.acquire();
try {
@@ -11457,7 +11469,8 @@
// If there is no default network, default network is considered active to keep the existing
// behavior. Initial value is used until first connect to the default network.
private volatile boolean mIsDefaultNetworkActive = true;
- private final ArrayMap<String, IdleTimerParams> mActiveIdleTimers = new ArrayMap<>();
+ // Key is netId. Value is configured idle timer information.
+ private final SparseArray<IdleTimerParams> mActiveIdleTimers = new SparseArray<>();
private static class IdleTimerParams {
public final int timeout;
@@ -11485,7 +11498,7 @@
public void handleReportNetworkActivity(NetworkActivityParams activityParams) {
ensureRunningOnConnectivityServiceThread();
- if (mActiveIdleTimers.isEmpty()) {
+ if (mActiveIdleTimers.size() == 0) {
// This activity change is not for the current default network.
// This can happen if netd callback post activity change event message but
// the default network is lost before processing this message.
@@ -11561,6 +11574,7 @@
*/
private boolean setupDataActivityTracking(NetworkAgentInfo networkAgent) {
final String iface = networkAgent.linkProperties.getInterfaceName();
+ final int netId = networkAgent.network().netId;
final int timeout;
final int type;
@@ -11585,7 +11599,7 @@
if (timeout > 0 && iface != null) {
try {
- mActiveIdleTimers.put(iface, new IdleTimerParams(timeout, type));
+ mActiveIdleTimers.put(netId, new IdleTimerParams(timeout, type));
mNetd.idletimerAddInterface(iface, timeout, Integer.toString(type));
return true;
} catch (Exception e) {
@@ -11601,6 +11615,7 @@
*/
private void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
final String iface = networkAgent.linkProperties.getInterfaceName();
+ final int netId = networkAgent.network().netId;
final NetworkCapabilities caps = networkAgent.networkCapabilities;
if (iface == null) return;
@@ -11616,11 +11631,12 @@
try {
updateRadioPowerState(false /* isActive */, type);
- final IdleTimerParams params = mActiveIdleTimers.remove(iface);
+ final IdleTimerParams params = mActiveIdleTimers.get(netId);
if (params == null) {
// IdleTimer is not added if the configured timeout is 0 or negative value
return;
}
+ mActiveIdleTimers.remove(netId);
// The call fails silently if no idle timer setup for this interface
mNetd.idletimerRemoveInterface(iface, params.timeout,
Integer.toString(params.transportType));
@@ -11691,9 +11707,9 @@
pw.print("mIsDefaultNetworkActive="); pw.println(mIsDefaultNetworkActive);
pw.println("Idle timers:");
try {
- for (Map.Entry<String, IdleTimerParams> ent : mActiveIdleTimers.entrySet()) {
- pw.print(" "); pw.print(ent.getKey()); pw.println(":");
- final IdleTimerParams params = ent.getValue();
+ for (int i = 0; i < mActiveIdleTimers.size(); i++) {
+ pw.print(" "); pw.print(mActiveIdleTimers.keyAt(i)); pw.println(":");
+ final IdleTimerParams params = mActiveIdleTimers.valueAt(i);
pw.print(" timeout="); pw.print(params.timeout);
pw.print(" type="); pw.println(params.transportType);
}
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
index c1ba40e..e16117b 100644
--- a/service/src/com/android/server/connectivity/ConnectivityNativeService.java
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -46,10 +46,6 @@
private static final String TAG = ConnectivityNativeService.class.getSimpleName();
private static final String CGROUP_PATH = "/sys/fs/cgroup";
- private static final String V4_PROG_PATH =
- "/sys/fs/bpf/net_shared/prog_block_bind4_block_port";
- private static final String V6_PROG_PATH =
- "/sys/fs/bpf/net_shared/prog_block_bind6_block_port";
private static final String BLOCKED_PORTS_MAP_PATH =
"/sys/fs/bpf/net_shared/map_block_blocked_ports_map";
@@ -95,7 +91,6 @@
protected ConnectivityNativeService(final Context context, @NonNull Dependencies deps) {
mContext = context;
mBpfBlockedPortsMap = deps.getBlockPortsMap();
- attachProgram();
}
@Override
@@ -155,23 +150,4 @@
public String getInterfaceHash() {
return this.HASH;
}
-
- /**
- * Attach BPF program
- */
- private void attachProgram() {
- try {
- BpfUtils.attachProgram(BPF_CGROUP_INET4_BIND, V4_PROG_PATH, CGROUP_PATH, 0);
- } catch (IOException e) {
- throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET4_BIND: "
- + e);
- }
- try {
- BpfUtils.attachProgram(BPF_CGROUP_INET6_BIND, V6_PROG_PATH, CGROUP_PATH, 0);
- } catch (IOException e) {
- throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET6_BIND: "
- + e);
- }
- Log.d(TAG, "Attached BPF_CGROUP_INET4_BIND and BPF_CGROUP_INET6_BIND programs");
- }
}
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index bdd841f..0f72cd4 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -453,6 +453,8 @@
* apply to the allowedUids field.
* They also should not mutate immutable capabilities, although for backward-compatibility
* this is not enforced and limited to just a log.
+ * Forbidden capabilities also make no sense for networks, so they are disallowed and
+ * will be ignored with a warning.
*
* @param carrierPrivilegeAuthenticator the authenticator, to check access UIDs.
*/
@@ -461,6 +463,7 @@
final NetworkCapabilities nc = new NetworkCapabilities(mDeclaredCapabilitiesUnsanitized);
if (nc.hasConnectivityManagedCapability()) {
Log.wtf(TAG, "BUG: " + this + " has CS-managed capability.");
+ nc.removeAllForbiddenCapabilities();
}
if (networkCapabilities.getOwnerUid() != nc.getOwnerUid()) {
Log.e(TAG, toShortString() + ": ignoring attempt to change owner from "
diff --git a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
index 59d655c..176b7bc 100644
--- a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
+++ b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
@@ -18,6 +18,8 @@
import static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IAPREFIX;
+import android.util.Log;
+
import com.android.net.module.util.Struct;
import com.android.net.module.util.Struct.Field;
import com.android.net.module.util.Struct.Type;
@@ -52,6 +54,7 @@
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
public class IaPrefixOption extends Struct {
+ private static final String TAG = IaPrefixOption.class.getSimpleName();
public static final int LENGTH = 25; // option length excluding IAprefix-options
@Field(order = 0, type = Type.S16)
@@ -78,6 +81,33 @@
}
/**
+ * Check whether or not IA Prefix option in IA_PD option is valid per RFC8415#section-21.22.
+ */
+ public boolean isValid(int t2) {
+ if (preferred < 0 || valid < 0) {
+ Log.w(TAG, "IA_PD option with invalid lifetime, preferred lifetime " + preferred
+ + ", valid lifetime " + valid);
+ return false;
+ }
+ if (preferred > valid) {
+ Log.w(TAG, "IA_PD option with preferred lifetime " + preferred
+ + " greater than valid lifetime " + valid);
+ return false;
+ }
+ if (prefixLen > 64) {
+ Log.w(TAG, "IA_PD option with prefix length " + prefixLen
+ + " longer than 64");
+ return false;
+ }
+ // Either preferred lifetime or t2 might be 0 which is valid, then ignore it.
+ if (preferred != 0 && t2 != 0 && preferred < t2) {
+ Log.w(TAG, "preferred lifetime " + preferred + " is smaller than T2 " + t2);
+ return false;
+ }
+ return true;
+ }
+
+ /**
* Build an IA_PD prefix option with given specific parameters.
*/
public static ByteBuffer build(final short length, final long preferred, final long valid,
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
index f7ffddb..81be37d 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
@@ -22,9 +22,15 @@
// Reject the packet
#define BPF_REJECT BPF_STMT(BPF_RET | BPF_K, 0)
+// Note arguments to BPF_JUMP(opcode, operand, true_offset, false_offset)
+
+// If not equal, jump over count instructions
+#define BPF_JUMP_IF_NOT_EQUAL(v, count) \
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 0, (count))
+
// *TWO* instructions: compare and if not equal jump over the accept statement
#define BPF2_ACCEPT_IF_EQUAL(v) \
- BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 0, 1), \
+ BPF_JUMP_IF_NOT_EQUAL((v), 1), \
BPF_ACCEPT
// *TWO* instructions: compare and if equal jump over the reject statement
@@ -32,6 +38,22 @@
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 1, 0), \
BPF_REJECT
+// *TWO* instructions: compare and if greater or equal jump over the reject statement
+#define BPF2_REJECT_IF_LESS_THAN(v) \
+ BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, (v), 1, 0), \
+ BPF_REJECT
+
+// *TWO* instructions: compare and if *NOT* greater jump over the reject statement
+#define BPF2_REJECT_IF_GREATER_THAN(v) \
+ BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, (v), 0, 1), \
+ BPF_REJECT
+
+// *THREE* instructions: compare and if *NOT* in range [lo, hi], jump over the reject statement
+#define BPF3_REJECT_IF_NOT_IN_RANGE(lo, hi) \
+ BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, (lo), 0, 1), \
+ BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, (hi), 0, 1), \
+ BPF_REJECT
+
// *TWO* instructions: compare and if none of the bits are set jump over the reject statement
#define BPF2_REJECT_IF_ANY_MASKED_BITS_SET(v) \
BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, (v), 0, 1), \
@@ -148,13 +170,13 @@
// HOPOPTS/DSTOPS follow up with 'u8 len', counting 8 byte units, (0->8, 1->16)
// *THREE* instructions
#define BPF3_LOAD_NETX_RELATIVE_V6EXTHDR_LEN \
- BPF_LOAD_NETX_RELATIVE_L4_U8(1) \
- BPF_STMT(BPF_ALU | BPF_ADD | BPF_K, 1) \
+ BPF_LOAD_NETX_RELATIVE_L4_U8(1), \
+ BPF_STMT(BPF_ALU | BPF_ADD | BPF_K, 1), \
BPF_STMT(BPF_ALU | BPF_LSH | BPF_K, 3)
// *TWO* instructions: A += X; X := A
#define BPF2_ADD_A_TO_X \
- BPF_STMT(BPF_ALU | BPF_ADD | BPF_X, 0) \
+ BPF_STMT(BPF_ALU | BPF_ADD | BPF_X, 0), \
BPF_STMT(BPF_MISC | BPF_TAX, 0)
// UDP/UDPLITE/TCP/SCTP/DCCP all start with be16 srcport, dstport
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index 67ac0e4..baff09b 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -105,9 +105,19 @@
* implemented in the kernel sources.
*/
-#define KVER_NONE 0
-#define KVER(a, b, c) (((a) << 24) + ((b) << 16) + (c))
-#define KVER_INF 0xFFFFFFFFu
+struct kver_uint { unsigned int kver; };
+#define KVER_(v) ((struct kver_uint){ .kver = (v) })
+#define KVER(a, b, c) KVER_(((a) << 24) + ((b) << 16) + (c))
+#define KVER_NONE KVER_(0)
+#define KVER_4_14 KVER(4, 14, 0)
+#define KVER_4_19 KVER(4, 19, 0)
+#define KVER_5_4 KVER(5, 4, 0)
+#define KVER_5_8 KVER(5, 8, 0)
+#define KVER_5_9 KVER(5, 9, 0)
+#define KVER_5_15 KVER(5, 15, 0)
+#define KVER_INF KVER_(0xFFFFFFFFu)
+
+#define KVER_IS_AT_LEAST(kver, a, b, c) ((kver).kver >= KVER(a, b, c).kver)
/*
* BPFFS (ie. /sys/fs/bpf) labelling is as follows:
@@ -188,10 +198,12 @@
__attribute__ ((section(".maps." #name), used)) \
____btf_map_##name = { }
-#define BPF_ASSERT_LOADER_VERSION(min_loader, ignore_eng, ignore_user, ignore_userdebug) \
- _Static_assert( \
- (min_loader) >= BPFLOADER_IGNORED_ON_VERSION || \
- !((ignore_eng) || (ignore_user) || (ignore_userdebug)), \
+#define BPF_ASSERT_LOADER_VERSION(min_loader, ignore_eng, ignore_user, ignore_userdebug) \
+ _Static_assert( \
+ (min_loader) >= BPFLOADER_IGNORED_ON_VERSION || \
+ !((ignore_eng).ignore_on_eng || \
+ (ignore_user).ignore_on_user || \
+ (ignore_userdebug).ignore_on_userdebug), \
"bpfloader min version must be >= 0.33 in order to use ignored_on");
#define DEFINE_BPF_MAP_BASE(the_map, TYPE, keysize, valuesize, num_entries, \
@@ -209,14 +221,14 @@
.mode = (md), \
.bpfloader_min_ver = (minloader), \
.bpfloader_max_ver = (maxloader), \
- .min_kver = (minkver), \
- .max_kver = (maxkver), \
+ .min_kver = (minkver).kver, \
+ .max_kver = (maxkver).kver, \
.selinux_context = (selinux), \
.pin_subdir = (pindir), \
- .shared = (share), \
- .ignore_on_eng = (ignore_eng), \
- .ignore_on_user = (ignore_user), \
- .ignore_on_userdebug = (ignore_userdebug), \
+ .shared = (share).shared, \
+ .ignore_on_eng = (ignore_eng).ignore_on_eng, \
+ .ignore_on_user = (ignore_user).ignore_on_user, \
+ .ignore_on_userdebug = (ignore_userdebug).ignore_on_userdebug, \
}; \
BPF_ASSERT_LOADER_VERSION(minloader, ignore_eng, ignore_user, ignore_userdebug);
@@ -230,7 +242,7 @@
selinux, pindir, share, min_loader, max_loader, \
ignore_eng, ignore_user, ignore_userdebug) \
DEFINE_BPF_MAP_BASE(the_map, RINGBUF, 0, 0, size_bytes, usr, grp, md, \
- selinux, pindir, share, KVER(5, 8, 0), KVER_INF, \
+ selinux, pindir, share, KVER_5_8, KVER_INF, \
min_loader, max_loader, ignore_eng, ignore_user, \
ignore_userdebug); \
\
@@ -312,11 +324,11 @@
#error "Bpf Map UID must be left at default of AID_ROOT for BpfLoader prior to v0.28"
#endif
-#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
- DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
- DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, false, \
- BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, /*ignore_on_eng*/false, \
- /*ignore_on_user*/false, /*ignore_on_userdebug*/false)
+#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
+ DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, PRIVATE, \
+ BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG, \
+ LOAD_ON_USER, LOAD_ON_USERDEBUG)
#define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
@@ -362,16 +374,16 @@
const struct bpf_prog_def SECTION("progs") the_prog##_def = { \
.uid = (prog_uid), \
.gid = (prog_gid), \
- .min_kver = (min_kv), \
- .max_kver = (max_kv), \
- .optional = (opt), \
+ .min_kver = (min_kv).kver, \
+ .max_kver = (max_kv).kver, \
+ .optional = (opt).optional, \
.bpfloader_min_ver = (min_loader), \
.bpfloader_max_ver = (max_loader), \
.selinux_context = (selinux), \
.pin_subdir = (pindir), \
- .ignore_on_eng = (ignore_eng), \
- .ignore_on_user = (ignore_user), \
- .ignore_on_userdebug = (ignore_userdebug), \
+ .ignore_on_eng = (ignore_eng).ignore_on_eng, \
+ .ignore_on_user = (ignore_user).ignore_on_user, \
+ .ignore_on_userdebug = (ignore_userdebug).ignore_on_userdebug, \
}; \
SECTION(SECTION_NAME) \
int the_prog
@@ -389,7 +401,7 @@
DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, opt, \
DEFAULT_BPF_PROG_SELINUX_CONTEXT, DEFAULT_BPF_PROG_PIN_SUBDIR, \
- false, false, false)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
// Programs (here used in the sense of functions/sections) marked optional are allowed to fail
// to load (for example due to missing kernel patches).
@@ -405,21 +417,24 @@
// programs requiring a kernel version >= min_kv && < max_kv
#define DEFINE_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
- false)
+ MANDATORY)
#define DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, \
max_kv) \
- DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, true)
+ DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
+ OPTIONAL)
// programs requiring a kernel version >= min_kv
#define DEFINE_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF, \
- false)
+ MANDATORY)
#define DEFINE_OPTIONAL_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF, \
- true)
+ OPTIONAL)
// programs with no kernel version requirements
#define DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, 0, KVER_INF, false)
+ DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF, \
+ MANDATORY)
#define DEFINE_OPTIONAL_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, 0, KVER_INF, true)
+ DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF, \
+ OPTIONAL)
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
index e7428b6..ef03c4d 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -114,6 +114,31 @@
// BPF wants 8, but 32-bit x86 wants 4
//_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+
+// for maps:
+struct shared_bool { bool shared; };
+#define PRIVATE ((struct shared_bool){ .shared = false })
+#define SHARED ((struct shared_bool){ .shared = true })
+
+// for programs:
+struct optional_bool { bool optional; };
+#define MANDATORY ((struct optional_bool){ .optional = false })
+#define OPTIONAL ((struct optional_bool){ .optional = true })
+
+// for both maps and programs:
+struct ignore_on_eng_bool { bool ignore_on_eng; };
+#define LOAD_ON_ENG ((struct ignore_on_eng_bool){ .ignore_on_eng = false })
+#define IGNORE_ON_ENG ((struct ignore_on_eng_bool){ .ignore_on_eng = true })
+
+struct ignore_on_user_bool { bool ignore_on_user; };
+#define LOAD_ON_USER ((struct ignore_on_user_bool){ .ignore_on_user = false })
+#define IGNORE_ON_USER ((struct ignore_on_user_bool){ .ignore_on_user = true })
+
+struct ignore_on_userdebug_bool { bool ignore_on_userdebug; };
+#define LOAD_ON_USERDEBUG ((struct ignore_on_userdebug_bool){ .ignore_on_userdebug = false })
+#define IGNORE_ON_USERDEBUG ((struct ignore_on_userdebug_bool){ .ignore_on_userdebug = true })
+
+
// Length of strings (incl. selinux_context and pin_subdir)
// in the bpf_map_def and bpf_prog_def structs.
//
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
index 35f22b9..46229b0 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
@@ -27,6 +27,9 @@
@Deprecated("Use Build.VERSION_CODES", ReplaceWith("Build.VERSION_CODES.S_V2"))
const val SC_V2 = Build.VERSION_CODES.S_V2
+// TODO: Remove this when Build.VERSION_CODES.VANILLA_ICE_CREAM is available in all branches
+// where this code builds
+const val VANILLA_ICE_CREAM = 35 // Bui1ld.VERSION_CODES.VANILLA_ICE_CREAM
private val MAX_TARGET_SDK_ANNOTATION_RE = Pattern.compile("MaxTargetSdk([0-9]+)$")
private val targetSdk = InstrumentationRegistry.getContext().applicationInfo.targetSdkVersion
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index 77383ad..6ea5347 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -29,6 +29,7 @@
"src/**/*.kt",
"src/**/*.aidl",
],
+ asset_dirs: ["assets"],
static_libs: [
"androidx.test.rules",
"mockito-target-minus-junit4",
diff --git a/tests/benchmark/assets/dataset/A052701.zip b/tests/benchmark/assets/dataset/A052701.zip
new file mode 100644
index 0000000..fdde1ad
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052701.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052801.zip b/tests/benchmark/assets/dataset/A052801.zip
new file mode 100644
index 0000000..7f908b7
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052801.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052802.zip b/tests/benchmark/assets/dataset/A052802.zip
new file mode 100644
index 0000000..180ad3e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052802.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052803.zip b/tests/benchmark/assets/dataset/A052803.zip
new file mode 100644
index 0000000..321a79b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052803.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052804.zip b/tests/benchmark/assets/dataset/A052804.zip
new file mode 100644
index 0000000..298ec04
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052804.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052901.zip b/tests/benchmark/assets/dataset/A052901.zip
new file mode 100644
index 0000000..0f49543
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052901.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052902.zip b/tests/benchmark/assets/dataset/A052902.zip
new file mode 100644
index 0000000..ec22456
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052902.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053001.zip b/tests/benchmark/assets/dataset/A053001.zip
new file mode 100644
index 0000000..ad5d82e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053001.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053002.zip b/tests/benchmark/assets/dataset/A053002.zip
new file mode 100644
index 0000000..8a4bb0c
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053002.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053003.zip b/tests/benchmark/assets/dataset/A053003.zip
new file mode 100644
index 0000000..24d2057
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053003.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053004.zip b/tests/benchmark/assets/dataset/A053004.zip
new file mode 100644
index 0000000..352f93f
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053004.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053005.zip b/tests/benchmark/assets/dataset/A053005.zip
new file mode 100644
index 0000000..2b49a1b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053005.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053006.zip b/tests/benchmark/assets/dataset/A053006.zip
new file mode 100644
index 0000000..a59f2ec
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053006.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053007.zip b/tests/benchmark/assets/dataset/A053007.zip
new file mode 100644
index 0000000..df7ae74
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053007.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053101.zip b/tests/benchmark/assets/dataset/A053101.zip
new file mode 100644
index 0000000..c10ed64
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053102.zip b/tests/benchmark/assets/dataset/A053102.zip
new file mode 100644
index 0000000..8c9f9cf
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053102.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053103.zip b/tests/benchmark/assets/dataset/A053103.zip
new file mode 100644
index 0000000..9202c50
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053103.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053104.zip b/tests/benchmark/assets/dataset/A053104.zip
new file mode 100644
index 0000000..3c77724
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053104.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060101.zip b/tests/benchmark/assets/dataset/A060101.zip
new file mode 100644
index 0000000..86443a7
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060102.zip b/tests/benchmark/assets/dataset/A060102.zip
new file mode 100644
index 0000000..4f2cf49
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060102.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060201.zip b/tests/benchmark/assets/dataset/A060201.zip
new file mode 100644
index 0000000..3c28bec
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060201.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060202.zip b/tests/benchmark/assets/dataset/A060202.zip
new file mode 100644
index 0000000..e39e493
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060202.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B053001.zip b/tests/benchmark/assets/dataset/B053001.zip
new file mode 100644
index 0000000..8408744
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B053001.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B053002.zip b/tests/benchmark/assets/dataset/B053002.zip
new file mode 100644
index 0000000..5245f70
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B053002.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060101.zip b/tests/benchmark/assets/dataset/B060101.zip
new file mode 100644
index 0000000..242c0d1
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060201.zip b/tests/benchmark/assets/dataset/B060201.zip
new file mode 100644
index 0000000..29df25a
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060201.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060202.zip b/tests/benchmark/assets/dataset/B060202.zip
new file mode 100644
index 0000000..bda9edd
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060202.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060203.zip b/tests/benchmark/assets/dataset/B060203.zip
new file mode 100644
index 0000000..b9fccfe
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060203.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060204.zip b/tests/benchmark/assets/dataset/B060204.zip
new file mode 100644
index 0000000..66227d2
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060204.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060205.zip b/tests/benchmark/assets/dataset/B060205.zip
new file mode 100644
index 0000000..6aaa06b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060205.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060206.zip b/tests/benchmark/assets/dataset/B060206.zip
new file mode 100644
index 0000000..18445b0
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060206.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060207.zip b/tests/benchmark/assets/dataset/B060207.zip
new file mode 100644
index 0000000..20f7c5b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060207.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060101.zip b/tests/benchmark/assets/dataset/C060101.zip
new file mode 100644
index 0000000..0b1c29f
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060102.zip b/tests/benchmark/assets/dataset/C060102.zip
new file mode 100644
index 0000000..8064905
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060102.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060103.zip b/tests/benchmark/assets/dataset/C060103.zip
new file mode 100644
index 0000000..d0e819f
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060103.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060104.zip b/tests/benchmark/assets/dataset/C060104.zip
new file mode 100644
index 0000000..f87ca8d
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060104.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060105.zip b/tests/benchmark/assets/dataset/C060105.zip
new file mode 100644
index 0000000..e869895
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060105.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060106.zip b/tests/benchmark/assets/dataset/C060106.zip
new file mode 100644
index 0000000..6d25a98
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060106.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060107.zip b/tests/benchmark/assets/dataset/C060107.zip
new file mode 100644
index 0000000..a7cb31c
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060107.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060108.zip b/tests/benchmark/assets/dataset/C060108.zip
new file mode 100644
index 0000000..c1a5898
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060108.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060109.zip b/tests/benchmark/assets/dataset/C060109.zip
new file mode 100644
index 0000000..bb9116e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060109.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060110.zip b/tests/benchmark/assets/dataset/C060110.zip
new file mode 100644
index 0000000..5ca0f96
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060110.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060111.zip b/tests/benchmark/assets/dataset/C060111.zip
new file mode 100644
index 0000000..6a12d7e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060111.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060112.zip b/tests/benchmark/assets/dataset/C060112.zip
new file mode 100644
index 0000000..fa2c30b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060112.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060113.zip b/tests/benchmark/assets/dataset/C060113.zip
new file mode 100644
index 0000000..63a34ba
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060113.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060114.zip b/tests/benchmark/assets/dataset/C060114.zip
new file mode 100644
index 0000000..bd60927
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060114.zip
Binary files differ
diff --git a/tests/benchmark/res/raw/netstats-many-uids-zip b/tests/benchmark/assets/dataset/netstats-many-uids.zip
similarity index 98%
rename from tests/benchmark/res/raw/netstats-many-uids-zip
rename to tests/benchmark/assets/dataset/netstats-many-uids.zip
index 22e8254..9554aaa 100644
--- a/tests/benchmark/res/raw/netstats-many-uids-zip
+++ b/tests/benchmark/assets/dataset/netstats-many-uids.zip
Binary files differ
diff --git a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
index e80548b..585157f 100644
--- a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
+++ b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
@@ -20,10 +20,9 @@
import android.net.NetworkStatsCollection
import android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID
import android.os.DropBoxManager
-import androidx.test.InstrumentationRegistry
+import androidx.test.platform.app.InstrumentationRegistry
import com.android.internal.util.FileRotator
import com.android.internal.util.FileRotator.Reader
-import com.android.server.connectivity.benchmarktests.R
import com.android.server.net.NetworkStatsRecorder
import java.io.BufferedInputStream
import java.io.DataInputStream
@@ -44,23 +43,22 @@
companion object {
private val DEFAULT_BUFFER_SIZE = 8192
private val FILE_CACHE_WARM_UP_REPEAT_COUNT = 10
- private val TEST_REPEAT_COUNT = 10
private val UID_COLLECTION_BUCKET_DURATION_MS = TimeUnit.HOURS.toMillis(2)
private val UID_RECORDER_ROTATE_AGE_MS = TimeUnit.DAYS.toMillis(15)
private val UID_RECORDER_DELETE_AGE_MS = TimeUnit.DAYS.toMillis(90)
+ private val TEST_DATASET_SUBFOLDER = "dataset/"
- private val testFilesDir by lazy {
- // These file generated by using real user dataset which has many uid records
- // and agreed to share the dataset for testing purpose. These dataset can be
- // extracted from rooted devices by using
- // "adb pull /data/misc/apexdata/com.android.tethering/netstats" command.
- val zipInputStream =
- ZipInputStream(getInputStreamForResource(R.raw.netstats_many_uids_zip))
- unzipToTempDir(zipInputStream)
- }
-
- private val uidTestFiles: List<File> by lazy {
- getSortedListForPrefix(testFilesDir, "uid")
+ // These files are generated by using real user dataset which has many uid records
+ // and agreed to share the dataset for testing purpose. These dataset can be
+ // extracted from rooted devices by using
+ // "adb pull /data/misc/apexdata/com.android.tethering/netstats" command.
+ private val testFilesAssets by lazy {
+ val zipFiles = context.assets.list(TEST_DATASET_SUBFOLDER)!!.asList()
+ zipFiles.map {
+ val zipInputStream =
+ ZipInputStream((TEST_DATASET_SUBFOLDER + it).toAssetInputStream())
+ File(unzipToTempDir(zipInputStream), "netstats")
+ }
}
// Test results shows the test cases who read the file first will take longer time to
@@ -72,24 +70,34 @@
@BeforeClass
fun setUpOnce() {
repeat(FILE_CACHE_WARM_UP_REPEAT_COUNT) {
- val collection = NetworkStatsCollection(UID_COLLECTION_BUCKET_DURATION_MS)
- for (file in uidTestFiles) {
- readFile(file, collection)
+ testFilesAssets.forEach {
+ val uidTestFiles = getSortedListForPrefix(it, "uid")
+ val collection = NetworkStatsCollection(UID_COLLECTION_BUCKET_DURATION_MS)
+ for (file in uidTestFiles) {
+ readFile(file, collection)
+ }
}
}
}
- private fun getInputStreamForResource(resourceId: Int): DataInputStream =
- DataInputStream(
- InstrumentationRegistry.getContext()
- .getResources().openRawResource(resourceId)
- )
+ val context get() = InstrumentationRegistry.getInstrumentation().getContext()
+ private fun String.toAssetInputStream() = DataInputStream(context.assets.open(this))
private fun unzipToTempDir(zis: ZipInputStream): File {
val statsDir =
Files.createTempDirectory(NetworkStatsTest::class.simpleName).toFile()
generateSequence { zis.nextEntry }.forEach { entry ->
- FileOutputStream(File(statsDir, entry.name)).use {
+ val entryFile = File(statsDir, entry.name)
+ if (entry.isDirectory) {
+ entryFile.mkdirs()
+ return@forEach
+ }
+
+ // Make sure all folders exists. There is no guarantee anywhere.
+ entryFile.parentFile!!.mkdirs()
+
+ // If the entry is a file extract it.
+ FileOutputStream(entryFile).use {
zis.copyTo(it, DEFAULT_BUFFER_SIZE)
}
}
@@ -99,7 +107,7 @@
// List [xt|uid|uid_tag].<start>-<end> files under the given directory.
private fun getSortedListForPrefix(statsDir: File, prefix: String): List<File> {
assertTrue(statsDir.exists())
- return statsDir.list() { dir, name -> name.startsWith("$prefix.") }
+ return statsDir.list { _, name -> name.startsWith("$prefix.") }
.orEmpty()
.map { it -> File(statsDir, it) }
.sorted()
@@ -115,7 +123,8 @@
fun testReadCollection_manyUids() {
// The file cache is warmed up by the @BeforeClass method, so now the test can repeat
// this a number of time to have a stable number.
- repeat(TEST_REPEAT_COUNT) {
+ testFilesAssets.forEach {
+ val uidTestFiles = getSortedListForPrefix(it, "uid")
val collection = NetworkStatsCollection(UID_COLLECTION_BUCKET_DURATION_MS)
for (file in uidTestFiles) {
readFile(file, collection)
@@ -127,10 +136,10 @@
fun testReadFromRecorder_manyUids() {
val mockObserver = mock<NonMonotonicObserver<String>>()
val mockDropBox = mock<DropBoxManager>()
- repeat(TEST_REPEAT_COUNT) {
+ testFilesAssets.forEach {
val recorder = NetworkStatsRecorder(
FileRotator(
- testFilesDir, PREFIX_UID, UID_RECORDER_ROTATE_AGE_MS, UID_RECORDER_DELETE_AGE_MS
+ it, PREFIX_UID, UID_RECORDER_ROTATE_AGE_MS, UID_RECORDER_DELETE_AGE_MS
),
mockObserver,
mockDropBox,
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index aae3425..bec9a4a 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -26,6 +26,7 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -63,6 +64,7 @@
import static com.android.modules.utils.build.SdkLevel.isAtLeastR;
import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.MiscAsserts.assertEmpty;
import static com.android.testutils.MiscAsserts.assertThrows;
@@ -369,6 +371,9 @@
.addCapability(NET_CAPABILITY_INTERNET)
.addCapability(NET_CAPABILITY_EIMS)
.addCapability(NET_CAPABILITY_NOT_METERED);
+ if (isAtLeastV()) {
+ netCap.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
+ }
if (isAtLeastS()) {
final ArraySet<Integer> allowedUids = new ArraySet<>();
allowedUids.add(4);
diff --git a/tests/cts/hostside/AndroidTest.xml b/tests/cts/hostside/AndroidTest.xml
index e83e36a..90b7875 100644
--- a/tests/cts/hostside/AndroidTest.xml
+++ b/tests/cts/hostside/AndroidTest.xml
@@ -33,6 +33,11 @@
<option name="teardown-command" value="cmd netpolicy stop-watching" />
</target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <option name="force-skip-system-props" value="true" />
+ <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" />
+ </target_preparer>
+
<test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
<option name="jar" value="CtsHostsideNetworkTests.jar" />
<option name="runtime-hint" value="3m56s" />
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 2245382..470bb17 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -38,7 +38,6 @@
srcs: ["src/**/*.java"],
// Tag this module as a cts test artifact
test_suites: [
- "cts",
"general-tests",
"sts",
],
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index 89a55a7..d92fb01 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -61,6 +61,7 @@
import android.util.Log;
import android.util.Pair;
+import com.android.compatibility.common.util.AmUtils;
import com.android.compatibility.common.util.BatteryUtils;
import com.android.compatibility.common.util.DeviceConfigStateHelper;
@@ -198,7 +199,8 @@
protected void tearDown() throws Exception {
executeShellCommand("cmd netpolicy stop-watching");
mServiceClient.unbind();
- if (mLock.isHeld()) mLock.release();
+ final PowerManager.WakeLock lock = mLock;
+ if (null != lock && lock.isHeld()) lock.release();
}
protected int getUid(String packageName) throws Exception {
@@ -719,10 +721,12 @@
Log.i(TAG, "Setting Battery Saver Mode to " + enabled);
if (enabled) {
turnBatteryOn();
+ AmUtils.waitForBroadcastBarrier();
executeSilentShellCommand("cmd power set-mode 1");
} else {
executeSilentShellCommand("cmd power set-mode 0");
turnBatteryOff();
+ AmUtils.waitForBroadcastBarrier();
}
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
index 8c38b44..5331601 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
@@ -449,13 +449,19 @@
// this function and using PollingCheck to try to make sure the uid has updated and reduce the
// flaky rate.
public static void assertNetworkingBlockedStatusForUid(int uid, boolean metered,
- boolean expectedResult) throws Exception {
- PollingCheck.waitFor(() -> (expectedResult == isUidNetworkingBlocked(uid, metered)));
+ boolean expectedResult) {
+ final String errMsg = String.format("Unexpected result from isUidNetworkingBlocked; "
+ + "uid= " + uid + ", metered=" + metered + ", expected=" + expectedResult);
+ PollingCheck.waitFor(() -> (expectedResult == isUidNetworkingBlocked(uid, metered)),
+ errMsg);
}
- public static void assertIsUidRestrictedOnMeteredNetworks(int uid, boolean expectedResult)
- throws Exception {
- PollingCheck.waitFor(() -> (expectedResult == isUidRestrictedOnMeteredNetworks(uid)));
+ public static void assertIsUidRestrictedOnMeteredNetworks(int uid, boolean expectedResult) {
+ final String errMsg = String.format(
+ "Unexpected result from isUidRestrictedOnMeteredNetworks; "
+ + "uid= " + uid + ", expected=" + expectedResult);
+ PollingCheck.waitFor(() -> (expectedResult == isUidRestrictedOnMeteredNetworks(uid)),
+ errMsg);
}
public static boolean isUidNetworkingBlocked(int uid, boolean meteredNetwork) {
diff --git a/tests/cts/hostside/app2/AndroidManifest.xml b/tests/cts/hostside/app2/AndroidManifest.xml
index ff7240d..2c2d957 100644
--- a/tests/cts/hostside/app2/AndroidManifest.xml
+++ b/tests/cts/hostside/app2/AndroidManifest.xml
@@ -20,6 +20,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@@ -45,7 +46,11 @@
<service android:name=".MyService"
android:exported="true"/>
<service android:name=".MyForegroundService"
- android:exported="true"/>
+ android:foregroundServiceType="specialUse"
+ android:exported="true">
+ <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+ android:value="Connectivity" />
+ </service>
<service android:name=".RemoteSocketFactoryService"
android:exported="true"/>
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 6de663a..b86de25 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -55,7 +55,7 @@
"junit-params",
"modules-utils-build",
"net-utils-framework-common",
- "truth-prebuilt",
+ "truth",
"TetheringIntegrationTestsBaseLib",
],
@@ -74,7 +74,10 @@
// devices.
android_test {
name: "CtsNetTestCases",
- defaults: ["CtsNetTestCasesDefaults", "ConnectivityNextEnableDefaults"],
+ defaults: [
+ "CtsNetTestCasesDefaults",
+ "ConnectivityNextEnableDefaults",
+ ],
static_libs: [
"DhcpPacketLib",
"NetworkStackApiCurrentShims",
@@ -129,7 +132,7 @@
}
android_test {
- name: "CtsNetTestCasesMaxTargetSdk33", // Must match CtsNetTestCasesMaxTargetSdk33 annotation.
+ name: "CtsNetTestCasesMaxTargetSdk33", // Must match CtsNetTestCasesMaxTargetSdk33 annotation.
defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
target_sdk_version: "33",
package_name: "android.net.cts.maxtargetsdk33",
@@ -137,17 +140,17 @@
}
android_test {
- name: "CtsNetTestCasesMaxTargetSdk31", // Must match CtsNetTestCasesMaxTargetSdk31 annotation.
+ name: "CtsNetTestCasesMaxTargetSdk31", // Must match CtsNetTestCasesMaxTargetSdk31 annotation.
defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
target_sdk_version: "31",
- package_name: "android.net.cts.maxtargetsdk31", // CTS package names must be unique.
+ package_name: "android.net.cts.maxtargetsdk31", // CTS package names must be unique.
instrumentation_target_package: "android.net.cts.maxtargetsdk31",
}
android_test {
- name: "CtsNetTestCasesMaxTargetSdk30", // Must match CtsNetTestCasesMaxTargetSdk30 annotation.
+ name: "CtsNetTestCasesMaxTargetSdk30", // Must match CtsNetTestCasesMaxTargetSdk30 annotation.
defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
target_sdk_version: "30",
- package_name: "android.net.cts.maxtargetsdk30", // CTS package names must be unique.
+ package_name: "android.net.cts.maxtargetsdk30", // CTS package names must be unique.
instrumentation_target_package: "android.net.cts.maxtargetsdk30",
}
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 9b81a56..1f1dd5d 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -41,7 +41,7 @@
"mockwebserver",
"junit",
"junit-params",
- "truth-prebuilt",
+ "truth",
],
platform_apis: true,
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 637ed26..594f3fb 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -19,8 +19,10 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL;
import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
@@ -28,6 +30,8 @@
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static com.android.testutils.DevSdkIgnoreRuleKt.VANILLA_ICE_CREAM;
+
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertArrayEquals;
@@ -104,6 +108,23 @@
verifyNoCapabilities(nr);
}
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testForbiddenCapabilities() {
+ final NetworkRequest.Builder builder = new NetworkRequest.Builder();
+ builder.addForbiddenCapability(NET_CAPABILITY_MMS);
+ assertTrue(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+ builder.removeForbiddenCapability(NET_CAPABILITY_MMS);
+ assertFalse(builder.build().hasCapability(NET_CAPABILITY_MMS));
+ builder.addCapability(NET_CAPABILITY_MMS);
+ assertFalse(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+ assertTrue(builder.build().hasCapability(NET_CAPABILITY_MMS));
+ builder.addForbiddenCapability(NET_CAPABILITY_MMS);
+ assertTrue(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+ assertFalse(builder.build().hasCapability(NET_CAPABILITY_MMS));
+ builder.clearCapabilities();
+ verifyNoCapabilities(builder.build());
+ }
+
@Test @IgnoreUpTo(Build.VERSION_CODES.Q)
public void testTemporarilyNotMeteredCapability() {
assertTrue(new NetworkRequest.Builder()
@@ -472,6 +493,32 @@
assertArrayEquals(netCapabilities, nr.getCapabilities());
}
+ @Test @IgnoreUpTo(VANILLA_ICE_CREAM)
+ public void testDefaultCapabilities() {
+ final NetworkRequest defaultNR = new NetworkRequest.Builder().build();
+ assertTrue(defaultNR.hasForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK));
+ assertFalse(defaultNR.hasCapability(NET_CAPABILITY_LOCAL_NETWORK));
+ assertTrue(defaultNR.hasCapability(NET_CAPABILITY_NOT_VPN));
+
+ final NetworkCapabilities emptyNC =
+ NetworkCapabilities.Builder.withoutDefaultCapabilities().build();
+ assertFalse(defaultNR.canBeSatisfiedBy(emptyNC));
+
+ // defaultNC represent the capabilities of a network agent, so they must not contain
+ // forbidden capabilities by default.
+ final NetworkCapabilities defaultNC = new NetworkCapabilities.Builder().build();
+ assertArrayEquals(new int[0], defaultNC.getForbiddenCapabilities());
+ // A default NR can be satisfied by default NC.
+ assertTrue(defaultNR.canBeSatisfiedBy(defaultNC));
+
+ // Conversely, network requests have forbidden capabilities by default to manage
+ // backward compatibility, so test that these forbidden capabilities are in place.
+ // Starting in V, NET_CAPABILITY_LOCAL_NETWORK is introduced but is not seen by
+ // default, thanks to a default forbidden capability in NetworkRequest.
+ defaultNC.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
+ assertFalse(defaultNR.canBeSatisfiedBy(defaultNC));
+ }
+
@Test
public void testBuildRequestFromExistingRequestWithBuilder() {
assumeTrue(TestUtils.shouldTestSApis());
diff --git a/tests/integration/AndroidManifest.xml b/tests/integration/AndroidManifest.xml
index 50f02d3..cea83c7 100644
--- a/tests/integration/AndroidManifest.xml
+++ b/tests/integration/AndroidManifest.xml
@@ -40,6 +40,8 @@
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<!-- Querying the resources package -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+ <!-- Register UidFrozenStateChangedCallback -->
+ <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
<application android:debuggable="true">
<uses-library android:name="android.test.runner"/>
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
index 0c424e9..cff4d6f 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/tests/mts/bpf_existence_test.cpp
@@ -40,6 +40,7 @@
#define TETHERING "/sys/fs/bpf/tethering/"
#define PRIVATE "/sys/fs/bpf/net_private/"
#define SHARED "/sys/fs/bpf/net_shared/"
+#define NETD_RO "/sys/fs/bpf/netd_readonly/"
#define NETD "/sys/fs/bpf/netd_shared/"
class BpfExistenceTest : public ::testing::Test {
@@ -119,9 +120,9 @@
};
// Provided by *current* mainline module for T+ devices with 5.4+ kernels
-static const set<string> MAINLINE_FOR_T_5_4_PLUS = {
- SHARED "prog_block_bind4_block_port",
- SHARED "prog_block_bind6_block_port",
+static const set<string> MAINLINE_FOR_T_4_19_PLUS = {
+ NETD_RO "prog_block_bind4_block_port",
+ NETD_RO "prog_block_bind6_block_port",
};
// Provided by *current* mainline module for T+ devices with 5.15+ kernels
@@ -176,7 +177,7 @@
// T still only requires Linux Kernel 4.9+.
DO_EXPECT(IsAtLeastT(), MAINLINE_FOR_T_PLUS);
DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 14, 0), MAINLINE_FOR_T_4_14_PLUS);
- DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_T_5_4_PLUS);
+ DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 19, 0), MAINLINE_FOR_T_4_19_PLUS);
DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 15, 0), MAINLINE_FOR_T_5_15_PLUS);
// U requires Linux Kernel 4.14+, but nothing (as yet) added or removed in U.
diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
index 2170882..1e1fd35 100644
--- a/tests/unit/java/android/net/NetworkStatsHistoryTest.java
+++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -54,6 +54,7 @@
import com.android.frameworks.tests.net.R;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.SkipPresubmit;
import org.junit.After;
import org.junit.Test;
@@ -343,6 +344,7 @@
}
+ @SkipPresubmit(reason = "Flaky: b/302325928; add to presubmit after fixing")
@Test
public void testFuzzing() throws Exception {
try {
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index eb03157..aa5e8b8 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -77,6 +77,7 @@
import static android.net.ConnectivityManager.TYPE_ETHERNET;
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL;
+import static android.net.ConnectivityManager.TYPE_NONE;
import static android.net.ConnectivityManager.TYPE_VPN;
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
@@ -418,7 +419,6 @@
import com.android.server.connectivity.UidRangeUtils;
import com.android.server.connectivity.Vpn;
import com.android.server.connectivity.VpnProfileStore;
-import com.android.server.net.LockdownVpnTracker;
import com.android.server.net.NetworkPinner;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -470,6 +470,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@@ -1499,15 +1500,7 @@
private int mVpnType = VpnManager.TYPE_VPN_SERVICE;
private UnderlyingNetworkInfo mUnderlyingNetworkInfo;
-
- // These ConditionVariables allow tests to wait for LegacyVpnRunner to be stopped/started.
- // TODO: this scheme is ad-hoc and error-prone because it does not fail if, for example, the
- // test expects two starts in a row, or even if the production code calls start twice in a
- // row. find a better solution. Simply putting a method to create a LegacyVpnRunner into
- // Vpn.Dependencies doesn't work because LegacyVpnRunner is not a static class and has
- // extensive access into the internals of Vpn.
- private ConditionVariable mStartLegacyVpnCv = new ConditionVariable();
- private ConditionVariable mStopVpnRunnerCv = new ConditionVariable();
+ private String mSessionKey;
public MockVpn(int userId) {
super(startHandlerThreadAndReturnLooper(), mServiceContext,
@@ -1668,36 +1661,52 @@
mInterface = null;
}
- @Override
- public void startLegacyVpnRunner() {
- mStartLegacyVpnCv.open();
+ private synchronized void startLegacyVpn() {
+ updateState(DetailedState.CONNECTING, "startLegacyVpn");
}
- public void expectStartLegacyVpnRunner() {
- assertTrue("startLegacyVpnRunner not called after " + TIMEOUT_MS + " ms",
- mStartLegacyVpnCv.block(TIMEOUT_MS));
-
- // startLegacyVpn calls stopVpnRunnerPrivileged, which will open mStopVpnRunnerCv, just
- // before calling startLegacyVpnRunner. Restore mStopVpnRunnerCv, so the test can expect
- // that the VpnRunner is stopped and immediately restarted by calling
- // expectStartLegacyVpnRunner() and expectStopVpnRunnerPrivileged() back-to-back.
- mStopVpnRunnerCv = new ConditionVariable();
+ // Mock the interaction of IkeV2VpnRunner start. In the context of ConnectivityService,
+ // setVpnDefaultForUids() is the main interaction and a sessionKey is stored.
+ private synchronized void startPlatformVpn() {
+ updateState(DetailedState.CONNECTING, "startPlatformVpn");
+ mSessionKey = UUID.randomUUID().toString();
+ // Assuming no disallowed applications
+ final Set<Range<Integer>> ranges = UidRange.toIntRanges(Set.of(PRIMARY_UIDRANGE));
+ mCm.setVpnDefaultForUids(mSessionKey, ranges);
+ // Wait for vpn network preference updates.
+ waitForIdle();
}
@Override
- public void stopVpnRunnerPrivileged() {
- if (mVpnRunner != null) {
- super.stopVpnRunnerPrivileged();
- disconnect();
- mStartLegacyVpnCv = new ConditionVariable();
+ public void startLegacyVpnPrivileged(VpnProfile profile,
+ @Nullable Network underlying, @NonNull LinkProperties egress) {
+ switch (profile.type) {
+ case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
+ case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
+ case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
+ case VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS:
+ startPlatformVpn();
+ break;
+ case VpnProfile.TYPE_L2TP_IPSEC_PSK:
+ case VpnProfile.TYPE_L2TP_IPSEC_RSA:
+ case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
+ case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
+ case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
+ startLegacyVpn();
+ break;
+ default:
+ fail("Unknown VPN profile type");
}
- mVpnRunner = null;
- mStopVpnRunnerCv.open();
}
- public void expectStopVpnRunnerPrivileged() {
- assertTrue("stopVpnRunnerPrivileged not called after " + TIMEOUT_MS + " ms",
- mStopVpnRunnerCv.block(TIMEOUT_MS));
+ @Override
+ public synchronized void stopVpnRunnerPrivileged() {
+ if (mSessionKey != null) {
+ // Clear vpn network preference.
+ mCm.setVpnDefaultForUids(mSessionKey, Collections.EMPTY_LIST);
+ mSessionKey = null;
+ }
+ disconnect();
}
@Override
@@ -1711,6 +1720,14 @@
UnderlyingNetworkInfo underlyingNetworkInfo) {
mUnderlyingNetworkInfo = underlyingNetworkInfo;
}
+
+ @Override
+ public synchronized boolean setUnderlyingNetworks(@Nullable Network[] networks) {
+ if (!mAgentRegistered) return false;
+ mMockNetworkAgent.setUnderlyingNetworks(
+ (networks == null) ? null : Arrays.asList(networks));
+ return true;
+ }
}
private UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
@@ -10192,7 +10209,7 @@
doAsUid(Process.SYSTEM_UID, () -> mCm.unregisterNetworkCallback(perUidCb));
}
- private VpnProfile setupLegacyLockdownVpn() {
+ private VpnProfile setupLockdownVpn(int profileType) {
final String profileName = "testVpnProfile";
final byte[] profileTag = profileName.getBytes(StandardCharsets.UTF_8);
doReturn(profileTag).when(mVpnProfileStore).get(Credentials.LOCKDOWN_VPN);
@@ -10201,7 +10218,9 @@
profile.name = "My VPN";
profile.server = "192.0.2.1";
profile.dnsServers = "8.8.8.8";
- profile.type = VpnProfile.TYPE_IPSEC_XAUTH_PSK;
+ profile.ipsecIdentifier = "My ipsecIdentifier";
+ profile.ipsecSecret = "My PSK";
+ profile.type = profileType;
final byte[] encodedProfile = profile.encode();
doReturn(encodedProfile).when(mVpnProfileStore).get(Credentials.VPN + profileName);
@@ -10219,8 +10238,8 @@
mMockVpn.connect(true);
}
- @Test
- public void testLegacyLockdownVpn() throws Exception {
+ private void doTestLockdownVpn(VpnProfile profile, boolean expectSetVpnDefaultForUids)
+ throws Exception {
mServiceContext.setPermission(
Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
@@ -10235,107 +10254,63 @@
mCm.registerSystemDefaultNetworkCallback(systemDefaultCallback,
new Handler(ConnectivityThread.getInstanceLooper()));
- // Pretend lockdown VPN was configured.
- final VpnProfile profile = setupLegacyLockdownVpn();
-
- // LockdownVpnTracker disables the Vpn teardown code and enables lockdown.
- // Check the VPN's state before it does so.
- assertTrue(mMockVpn.getEnableTeardown());
- assertFalse(mMockVpn.getLockdown());
-
- // VMSHandlerThread was used inside VpnManagerService and taken into LockDownVpnTracker.
- // VpnManagerService was decoupled from this test but this handlerThread is still required
- // in LockDownVpnTracker. Keep it until LockDownVpnTracker related verification is moved to
- // its own test.
- final HandlerThread VMSHandlerThread = new HandlerThread("TestVpnManagerService");
- VMSHandlerThread.start();
-
- // LockdownVpnTracker is created from VpnManagerService but VpnManagerService is decoupled
- // from ConnectivityServiceTest. Create it directly to simulate LockdownVpnTracker is
- // created.
- // TODO: move LockdownVpnTracker related tests to its own test.
- // Lockdown VPN disables teardown and enables lockdown.
- final LockdownVpnTracker lockdownVpnTracker = new LockdownVpnTracker(mServiceContext,
- VMSHandlerThread.getThreadHandler(), mMockVpn, profile);
- lockdownVpnTracker.init();
- assertFalse(mMockVpn.getEnableTeardown());
- assertTrue(mMockVpn.getLockdown());
+ // Init lockdown state to simulate LockdownVpnTracker behavior.
+ mCm.setLegacyLockdownVpnEnabled(true);
+ mMockVpn.setEnableTeardown(false);
+ final Set<Range<Integer>> ranges = UidRange.toIntRanges(Set.of(PRIMARY_UIDRANGE));
+ mCm.setRequireVpnForUids(true /* requireVpn */, ranges);
// Bring up a network.
- // Expect nothing to happen because the network does not have an IPv4 default route: legacy
- // VPN only supports IPv4.
final LinkProperties cellLp = new LinkProperties();
cellLp.setInterfaceName("rmnet0");
- cellLp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
- cellLp.addRoute(new RouteInfo(new IpPrefix("::/0"), null, "rmnet0"));
- mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
- mCellAgent.connect(false /* validated */);
- callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
- defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
- systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
- waitForIdle();
- assertNull(mMockVpn.getAgent());
-
- // Add an IPv4 address. Ideally the VPN should start, but it doesn't because nothing calls
- // LockdownVpnTracker#handleStateChangedLocked. This is a bug.
- // TODO: consider fixing this.
cellLp.addLinkAddress(new LinkAddress("192.0.2.2/25"));
cellLp.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0"), null, "rmnet0"));
- mCellAgent.sendLinkProperties(cellLp);
- callback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
- defaultCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
- systemDefaultCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
- waitForIdle();
- assertNull(mMockVpn.getAgent());
-
- // Disconnect, then try again with a network that supports IPv4 at connection time.
- // Expect lockdown VPN to come up.
- ExpectedBroadcast b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
- mCellAgent.disconnect();
- callback.expect(LOST, mCellAgent);
- defaultCallback.expect(LOST, mCellAgent);
- systemDefaultCallback.expect(LOST, mCellAgent);
- b1.expectBroadcast();
-
// When lockdown VPN is active, the NetworkInfo state in CONNECTIVITY_ACTION is overwritten
// with the state of the VPN network. So expect a CONNECTING broadcast.
- b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTING);
+ final ExpectedBroadcast b = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTING);
mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
mCellAgent.connect(false /* validated */);
callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
- b1.expectBroadcast();
- assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+ b.expectBroadcast();
+ // Simulate LockdownVpnTracker attempting to start the VPN since it received the
+ // systemDefault callback.
+ mMockVpn.startLegacyVpnPrivileged(profile, mCellAgent.getNetwork(), cellLp);
+ if (expectSetVpnDefaultForUids) {
+ // setVpnDefaultForUids() releases the original network request and creates a VPN
+ // request so LOST callback is received.
+ defaultCallback.expect(LOST, mCellAgent);
+ // Due to the VPN default request, getActiveNetworkInfo() gets the mNoServiceNetwork
+ // as the network satisfier.
+ assertNull(mCm.getActiveNetworkInfo());
+ } else {
+ assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
assertNetworkInfo(TYPE_VPN, DetailedState.BLOCKED);
assertExtraInfoFromCmBlocked(mCellAgent);
- // TODO: it would be nice if we could simply rely on the production code here, and have
- // LockdownVpnTracker start the VPN, have the VPN code register its NetworkAgent with
- // ConnectivityService, etc. That would require duplicating a fair bit of code from the
- // Vpn tests around how to mock out LegacyVpnRunner. But even if we did that, this does not
- // work for at least two reasons:
- // 1. In this test, calling registerNetworkAgent does not actually result in an agent being
- // registered. This is because nothing calls onNetworkMonitorCreated, which is what
- // actually ends up causing handleRegisterNetworkAgent to be called. Code in this test
- // that wants to register an agent must use TestNetworkAgentWrapper.
- // 2. Even if we exposed Vpn#agentConnect to the test, and made MockVpn#agentConnect call
- // the TestNetworkAgentWrapper code, this would deadlock because the
- // TestNetworkAgentWrapper code cannot be called on the handler thread since it calls
- // waitForIdle().
- mMockVpn.expectStartLegacyVpnRunner();
- b1 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
- ExpectedBroadcast b2 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED);
+ final ExpectedBroadcast b2 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
+ final ExpectedBroadcast b3 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED);
establishLegacyLockdownVpn(mCellAgent.getNetwork());
callback.expectAvailableThenValidatedCallbacks(mMockVpn);
defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
systemDefaultCallback.assertNoCallback();
- NetworkCapabilities vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
- b1.expectBroadcast();
+ final NetworkCapabilities vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
b2.expectBroadcast();
- assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+ b3.expectBroadcast();
+ if (expectSetVpnDefaultForUids) {
+ // Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
+ // network satisfier which has TYPE_VPN.
+ assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+ } else {
+ // LegacyVpnRunner does not call setVpnDefaultsForUids(), which means
+ // getActiveNetworkInfo() can only return the info for the system-wide default instead.
+ // This should be fixed, but LegacyVpnRunner will be removed soon anyway.
+ assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED);
assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
@@ -10356,53 +10331,78 @@
wifiNc.addCapability(NET_CAPABILITY_NOT_METERED);
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp, wifiNc);
- b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
+ final ExpectedBroadcast b4 =
+ expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
// Wifi is CONNECTING because the VPN isn't up yet.
- b2 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTING);
- ExpectedBroadcast b3 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
+ final ExpectedBroadcast b5 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTING);
mWiFiAgent.connect(false /* validated */);
- b1.expectBroadcast();
- b2.expectBroadcast();
- b3.expectBroadcast();
- mMockVpn.expectStopVpnRunnerPrivileged();
- mMockVpn.expectStartLegacyVpnRunner();
-
- // TODO: why is wifi not blocked? Is it because when this callback is sent, the VPN is still
- // connected, so the network is not considered blocked by the lockdown UID ranges? But the
- // fact that a VPN is connected should only result in the VPN itself being unblocked, not
- // any other network. Bug in isUidBlockedByVpn?
+ // Wifi is not blocked since VPN network is still connected.
callback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+ defaultCallback.assertNoCallback();
+ systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+ b4.expectBroadcast();
+ b5.expectBroadcast();
+
+ // Simulate LockdownVpnTracker restarting the VPN since it received the systemDefault
+ // callback with different network.
+ final ExpectedBroadcast b6 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
+ mMockVpn.stopVpnRunnerPrivileged();
+ mMockVpn.startLegacyVpnPrivileged(profile, mWiFiAgent.getNetwork(), wifiLp);
+ // VPN network is disconnected (to restart)
callback.expect(LOST, mMockVpn);
defaultCallback.expect(LOST, mMockVpn);
+ // The network preference is cleared when VPN is disconnected so it receives callbacks for
+ // the system-wide default.
defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiAgent);
- systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+ if (expectSetVpnDefaultForUids) {
+ // setVpnDefaultForUids() releases the original network request and creates a VPN
+ // request so LOST callback is received.
+ defaultCallback.expect(LOST, mWiFiAgent);
+ }
+ systemDefaultCallback.assertNoCallback();
+ b6.expectBroadcast();
// While the VPN is reconnecting on the new network, everything is blocked.
- assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+ if (expectSetVpnDefaultForUids) {
+ // Due to the VPN default request, getActiveNetworkInfo() gets the mNoServiceNetwork
+ // as the network satisfier.
+ assertNull(mCm.getActiveNetworkInfo());
+ } else {
+ assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
assertNetworkInfo(TYPE_VPN, DetailedState.BLOCKED);
assertExtraInfoFromCmBlocked(mWiFiAgent);
// The VPN comes up again on wifi.
- b1 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
- b2 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED);
+ final ExpectedBroadcast b7 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
+ final ExpectedBroadcast b8 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED);
establishLegacyLockdownVpn(mWiFiAgent.getNetwork());
callback.expectAvailableThenValidatedCallbacks(mMockVpn);
defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
systemDefaultCallback.assertNoCallback();
- b1.expectBroadcast();
- b2.expectBroadcast();
- assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+ b7.expectBroadcast();
+ b8.expectBroadcast();
+ if (expectSetVpnDefaultForUids) {
+ // Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
+ // network satisfier which has TYPE_VPN.
+ assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+ } else {
+ // LegacyVpnRunner does not call setVpnDefaultsForUids(), which means
+ // getActiveNetworkInfo() can only return the info for the system-wide default instead.
+ // This should be fixed, but LegacyVpnRunner will be removed soon anyway.
+ assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
assertExtraInfoFromCmPresent(mWiFiAgent);
- vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
- assertTrue(vpnNc.hasTransport(TRANSPORT_VPN));
- assertTrue(vpnNc.hasTransport(TRANSPORT_WIFI));
- assertFalse(vpnNc.hasTransport(TRANSPORT_CELLULAR));
- assertTrue(vpnNc.hasCapability(NET_CAPABILITY_NOT_METERED));
+ final NetworkCapabilities vpnNc2 = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
+ assertTrue(vpnNc2.hasTransport(TRANSPORT_VPN));
+ assertTrue(vpnNc2.hasTransport(TRANSPORT_WIFI));
+ assertFalse(vpnNc2.hasTransport(TRANSPORT_CELLULAR));
+ assertTrue(vpnNc2.hasCapability(NET_CAPABILITY_NOT_METERED));
// Disconnect cell. Nothing much happens since it's not the default network.
mCellAgent.disconnect();
@@ -10410,25 +10410,55 @@
defaultCallback.assertNoCallback();
systemDefaultCallback.assertNoCallback();
- assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+ if (expectSetVpnDefaultForUids) {
+ // Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
+ // network satisfier which has TYPE_VPN.
+ assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+ } else {
+ // LegacyVpnRunner does not call setVpnDefaultsForUids(), which means
+ // getActiveNetworkInfo() can only return the info for the system-wide default instead.
+ // This should be fixed, but LegacyVpnRunner will be removed soon anyway.
+ assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
assertExtraInfoFromCmPresent(mWiFiAgent);
- b1 = expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED);
- b2 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
+ final ExpectedBroadcast b9 =
+ expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED);
+ final ExpectedBroadcast b10 =
+ expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
mWiFiAgent.disconnect();
callback.expect(LOST, mWiFiAgent);
- systemDefaultCallback.expect(LOST, mWiFiAgent);
- b1.expectBroadcast();
callback.expectCaps(mMockVpn, c -> !c.hasTransport(TRANSPORT_WIFI));
- mMockVpn.expectStopVpnRunnerPrivileged();
+ defaultCallback.expectCaps(mMockVpn, c -> !c.hasTransport(TRANSPORT_WIFI));
+ systemDefaultCallback.expect(LOST, mWiFiAgent);
+ // TODO: There should only be one LOST callback. Since the WIFI network is underlying a VPN
+ // network, ConnectivityService#propagateUnderlyingNetworkCapabilities() causes a rematch to
+ // occur. Notably, this happens before setting the satisfiers of its network requests to
+ // null. Since the satisfiers are set to null in the rematch, an extra LOST callback is
+ // called.
+ systemDefaultCallback.expect(LOST, mWiFiAgent);
+ b9.expectBroadcast();
+ mMockVpn.stopVpnRunnerPrivileged();
callback.expect(LOST, mMockVpn);
- b2.expectBroadcast();
+ defaultCallback.expect(LOST, mMockVpn);
+ b10.expectBroadcast();
- VMSHandlerThread.quitSafely();
- VMSHandlerThread.join();
+ assertNoCallbacks(callback, defaultCallback, systemDefaultCallback);
+ }
+
+ @Test
+ public void testLockdownVpn_LegacyVpnRunner() throws Exception {
+ final VpnProfile profile = setupLockdownVpn(VpnProfile.TYPE_IPSEC_XAUTH_PSK);
+ doTestLockdownVpn(profile, false /* expectSetVpnDefaultForUids */);
+ }
+
+ @Test
+ public void testLockdownVpn_Ikev2VpnRunner() throws Exception {
+ final VpnProfile profile = setupLockdownVpn(VpnProfile.TYPE_IKEV2_IPSEC_PSK);
+ doTestLockdownVpn(profile, true /* expectSetVpnDefaultForUids */);
}
@Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 56346ad..d674767 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -1839,6 +1839,22 @@
// a subsequent CL.
}
+ @Test
+ public void testStartLegacyVpnIpv6() throws Exception {
+ setMockedUsers(PRIMARY_USER);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(EGRESS_IFACE);
+ lp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+ final RouteInfo defaultRoute = new RouteInfo(
+ new IpPrefix(Inet6Address.ANY, 0), null, EGRESS_IFACE);
+ lp.addRoute(defaultRoute);
+
+ // IllegalStateException thrown since legacy VPN only supports IPv4.
+ assertThrows(IllegalStateException.class,
+ () -> vpn.startLegacyVpn(mVpnProfile, EGRESS_NETWORK, lp));
+ }
+
private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
setMockedUsers(PRIMARY_USER);
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
index c39ee1e..2797462 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
@@ -82,7 +82,8 @@
@Test
fun testAnnounce() {
- val replySender = MdnsReplySender( thread.looper, socket, buffer, sharedLog)
+ val replySender = MdnsReplySender(
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
@Suppress("UNCHECKED_CAST")
val cb = mock(MdnsPacketRepeater.PacketRepeaterCallback::class.java)
as MdnsPacketRepeater.PacketRepeaterCallback<BaseAnnouncementInfo>
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index 118ca47..db41a6a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -190,8 +190,8 @@
fun testReplyToQuery() {
addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- val mockReply = mock(MdnsRecordRepository.ReplyInfo::class.java)
- doReturn(mockReply).`when`(repository).getReply(any(), any())
+ val testReply = MdnsReplyInfo(emptyList(), emptyList(), 0, InetSocketAddress(0))
+ doReturn(testReply).`when`(repository).getReply(any(), any())
// Query obtained with:
// scapy.raw(scapy.DNS(
@@ -216,7 +216,7 @@
assertContentEquals(arrayOf("_testservice", "_tcp", "local"), it.questions[0].name)
}
- verify(replySender).queueReply(mockReply)
+ verify(replySender).queueReply(testReply)
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
index 3701b0c..8917ed3 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -42,6 +42,7 @@
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.HandlerUtils;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -69,6 +70,7 @@
@Mock private SocketCreationCallback mSocketCreationCallback;
@Mock private SharedLog mSharedLog;
private MdnsMultinetworkSocketClient mSocketClient;
+ private HandlerThread mHandlerThread;
private Handler mHandler;
private SocketKey mSocketKey;
@@ -76,14 +78,23 @@
public void setUp() throws SocketException {
MockitoAnnotations.initMocks(this);
- final HandlerThread thread = new HandlerThread("MdnsMultinetworkSocketClientTest");
- thread.start();
- mHandler = new Handler(thread.getLooper());
+ mHandlerThread = new HandlerThread("MdnsMultinetworkSocketClientTest");
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
mSocketKey = new SocketKey(1000 /* interfaceIndex */);
- mSocketClient = new MdnsMultinetworkSocketClient(thread.getLooper(), mProvider, mSharedLog);
+ mSocketClient = new MdnsMultinetworkSocketClient(
+ mHandlerThread.getLooper(), mProvider, mSharedLog);
mHandler.post(() -> mSocketClient.setCallback(mCallback));
}
+ @After
+ public void tearDown() throws Exception {
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
+ }
+ }
+
private SocketCallback expectSocketCallback() {
return expectSocketCallback(mListener, mNetwork);
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
index b667e5f..28ea4b6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
@@ -32,7 +32,7 @@
// Probe packet with 1 question for Android.local, and 4 additionalRecords with 4 addresses
// for Android.local (similar to legacy mdnsresponder probes, although it used to put 4
// identical questions(!!) for Android.local when there were 4 addresses).
- val packetHex = "00000000000100000004000007416e64726f6964056c6f63616c0000ff0001c00c000100" +
+ val packetHex = "007b0000000100000004000007416e64726f6964056c6f63616c0000ff0001c00c000100" +
"01000000780004c000027bc00c001c000100000078001020010db8000000000000000000000123c0" +
"0c001c000100000078001020010db8000000000000000000000456c00c001c000100000078001020" +
"010db8000000000000000000000789"
@@ -41,6 +41,7 @@
val reader = MdnsPacketReader(bytes, bytes.size)
val packet = MdnsPacket.parse(reader)
+ assertEquals(123, packet.transactionId)
assertEquals(1, packet.questions.size)
assertEquals(0, packet.answers.size)
assertEquals(4, packet.authorityRecords.size)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
index f284819..5b7c0ba 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
@@ -119,7 +119,8 @@
@Test
fun testProbe() {
- val replySender = MdnsReplySender(thread.looper, socket, buffer, sharedLog)
+ val replySender = MdnsReplySender(
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(
listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890)))
@@ -143,7 +144,8 @@
@Test
fun testProbeMultipleRecords() {
- val replySender = MdnsReplySender(thread.looper, socket, buffer, sharedLog)
+ val replySender = MdnsReplySender(
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(listOf(
makeServiceRecord(TEST_SERVICE_NAME_1, 37890),
@@ -181,7 +183,8 @@
@Test
fun testStopProbing() {
- val replySender = MdnsReplySender(thread.looper, socket, buffer, sharedLog)
+ val replySender = MdnsReplySender(
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(
listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890)),
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index d26a74d..f26f7e1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -106,6 +106,7 @@
assertEquals(TEST_SERVICE_ID_1, probingInfo.serviceId)
val packet = probingInfo.getPacket(0)
+ assertEquals(0, packet.transactionId)
assertEquals(MdnsConstants.FLAGS_QUERY, packet.flags)
assertEquals(0, packet.answers.size)
assertEquals(0, packet.additionalRecords.size)
@@ -174,6 +175,7 @@
assertEquals(1, repository.servicesCount)
val packet = exitAnnouncement.getPacket(0)
+ assertEquals(0, packet.transactionId)
assertEquals(0x8400 /* response, authoritative */, packet.flags)
assertEquals(0, packet.questions.size)
assertEquals(0, packet.authorityRecords.size)
@@ -203,6 +205,7 @@
assertEquals(1, repository.servicesCount)
val packet = exitAnnouncement.getPacket(0)
+ assertEquals(0, packet.transactionId)
assertEquals(0x8400 /* response, authoritative */, packet.flags)
assertEquals(0, packet.questions.size)
assertEquals(0, packet.authorityRecords.size)
@@ -250,6 +253,7 @@
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
val packet = announcementInfo.getPacket(0)
+ assertEquals(0, packet.transactionId)
assertEquals(0x8400 /* response, authoritative */, packet.flags)
assertEquals(0, packet.questions.size)
assertEquals(0, packet.authorityRecords.size)
@@ -373,6 +377,7 @@
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
val serviceType = arrayOf("_testservice", "_tcp", "local")
val offloadPacket = repository.getOffloadPacket(TEST_SERVICE_ID_1)
+ assertEquals(0, offloadPacket.transactionId)
assertEquals(0x8400, offloadPacket.flags)
assertEquals(0, offloadPacket.questions.size)
assertEquals(0, offloadPacket.additionalRecords.size)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
index b43bcf7..1b6f120 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
@@ -19,6 +19,7 @@
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
+import com.android.server.connectivity.mdns.MdnsServiceCache.CacheKey
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import java.util.concurrent.CompletableFuture
@@ -43,6 +44,8 @@
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsServiceCacheTest {
private val socketKey = SocketKey(null /* network */, INTERFACE_INDEX)
+ private val cacheKey1 = CacheKey(SERVICE_TYPE_1, socketKey)
+ private val cacheKey2 = CacheKey(SERVICE_TYPE_2, socketKey)
private val thread = HandlerThread(MdnsServiceCacheTest::class.simpleName)
private val handler by lazy {
Handler(thread.looper)
@@ -69,47 +72,36 @@
return future.get(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
}
- private fun addOrUpdateService(
- serviceType: String,
- socketKey: SocketKey,
- service: MdnsResponse
- ): Unit = runningOnHandlerAndReturn {
- serviceCache.addOrUpdateService(serviceType, socketKey, service)
- }
+ private fun addOrUpdateService(cacheKey: CacheKey, service: MdnsResponse): Unit =
+ runningOnHandlerAndReturn { serviceCache.addOrUpdateService(cacheKey, service) }
- private fun removeService(serviceName: String, serviceType: String, socketKey: SocketKey):
- Unit = runningOnHandlerAndReturn {
- serviceCache.removeService(serviceName, serviceType, socketKey) }
+ private fun removeService(serviceName: String, cacheKey: CacheKey): Unit =
+ runningOnHandlerAndReturn { serviceCache.removeService(serviceName, cacheKey) }
- private fun getService(serviceName: String, serviceType: String, socketKey: SocketKey):
- MdnsResponse? = runningOnHandlerAndReturn {
- serviceCache.getCachedService(serviceName, serviceType, socketKey) }
+ private fun getService(serviceName: String, cacheKey: CacheKey): MdnsResponse? =
+ runningOnHandlerAndReturn { serviceCache.getCachedService(serviceName, cacheKey) }
- private fun getServices(serviceType: String, socketKey: SocketKey): List<MdnsResponse> =
- runningOnHandlerAndReturn { serviceCache.getCachedServices(serviceType, socketKey) }
+ private fun getServices(cacheKey: CacheKey): List<MdnsResponse> =
+ runningOnHandlerAndReturn { serviceCache.getCachedServices(cacheKey) }
@Test
fun testAddAndRemoveService() {
- addOrUpdateService(
- SERVICE_TYPE_1, socketKey, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
- var response = getService(SERVICE_NAME_1, SERVICE_TYPE_1, socketKey)
+ addOrUpdateService(cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
+ var response = getService(SERVICE_NAME_1, cacheKey1)
assertNotNull(response)
assertEquals(SERVICE_NAME_1, response.serviceInstanceName)
- removeService(SERVICE_NAME_1, SERVICE_TYPE_1, socketKey)
- response = getService(SERVICE_NAME_1, SERVICE_TYPE_1, socketKey)
+ removeService(SERVICE_NAME_1, cacheKey1)
+ response = getService(SERVICE_NAME_1, cacheKey1)
assertNull(response)
}
@Test
fun testGetCachedServices_multipleServiceTypes() {
- addOrUpdateService(
- SERVICE_TYPE_1, socketKey, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
- addOrUpdateService(
- SERVICE_TYPE_1, socketKey, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1))
- addOrUpdateService(
- SERVICE_TYPE_2, socketKey, createResponse(SERVICE_NAME_2, SERVICE_TYPE_2))
+ addOrUpdateService(cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
+ addOrUpdateService(cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1))
+ addOrUpdateService(cacheKey2, createResponse(SERVICE_NAME_2, SERVICE_TYPE_2))
- val responses1 = getServices(SERVICE_TYPE_1, socketKey)
+ val responses1 = getServices(cacheKey1)
assertEquals(2, responses1.size)
assertTrue(responses1.stream().anyMatch { response ->
response.serviceInstanceName == SERVICE_NAME_1
@@ -117,19 +109,19 @@
assertTrue(responses1.any { response ->
response.serviceInstanceName == SERVICE_NAME_2
})
- val responses2 = getServices(SERVICE_TYPE_2, socketKey)
+ val responses2 = getServices(cacheKey2)
assertEquals(1, responses2.size)
assertTrue(responses2.any { response ->
response.serviceInstanceName == SERVICE_NAME_2
})
- removeService(SERVICE_NAME_2, SERVICE_TYPE_1, socketKey)
- val responses3 = getServices(SERVICE_TYPE_1, socketKey)
+ removeService(SERVICE_NAME_2, cacheKey1)
+ val responses3 = getServices(cacheKey1)
assertEquals(1, responses3.size)
assertTrue(responses3.any { response ->
response.serviceInstanceName == SERVICE_NAME_1
})
- val responses4 = getServices(SERVICE_TYPE_2, socketKey)
+ val responses4 = getServices(cacheKey2)
assertEquals(1, responses4.size)
assertTrue(responses4.any { response ->
response.serviceInstanceName == SERVICE_NAME_2
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
index 6f8ba6c..58f20a9 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
@file:Suppress("DEPRECATION") // This file tests a bunch of deprecated methods : don't warn about it
package com.android.server
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
new file mode 100644
index 0000000..86426c2
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.server
+
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_DOWNSTREAM_NETWORK
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class CSKeepConnectedTest : CSTest() {
+ @Test
+ fun testKeepConnectedLocalAgent() {
+ deps.setBuildSdk(VERSION_V)
+ val nc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build()
+ val keepConnectedAgent = Agent(nc = nc, score = FromS(NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_DOWNSTREAM_NETWORK)
+ .build()))
+ val dontKeepConnectedAgent = Agent(nc = nc)
+ doTestKeepConnected(keepConnectedAgent, dontKeepConnectedAgent)
+ }
+
+ @Test
+ fun testKeepConnectedForTest() {
+ val keepAgent = Agent(score = FromS(NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+ .build()))
+ val dontKeepAgent = Agent()
+ doTestKeepConnected(keepAgent, dontKeepAgent)
+ }
+
+ fun doTestKeepConnected(keepAgent: CSAgentWrapper, dontKeepAgent: CSAgentWrapper) {
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+ keepAgent.connect()
+ dontKeepAgent.connect()
+
+ cb.expectAvailableCallbacks(keepAgent.network, validated = false)
+ cb.expectAvailableCallbacks(dontKeepAgent.network, validated = false)
+
+ // After the nascent timer, the agent without keep connected gets lost.
+ cb.expect<Lost>(dontKeepAgent.network)
+ cb.assertNoCallback()
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index a5a1aeb..1d0d5df 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
package com.android.server
import android.content.Context
@@ -19,6 +35,7 @@
import android.os.HandlerThread
import com.android.modules.utils.build.SdkLevel
import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
import com.android.testutils.TestableNetworkCallback
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
@@ -31,6 +48,8 @@
import kotlin.test.assertEquals
import kotlin.test.fail
+const val SHORT_TIMEOUT_MS = 200L
+
private inline fun <reified T> ArgumentCaptor() = ArgumentCaptor.forClass(T::class.java)
private val agentCounter = AtomicInteger(1)
@@ -44,6 +63,7 @@
*/
class CSAgentWrapper(
val context: Context,
+ val deps: ConnectivityService.Dependencies,
csHandlerThread: HandlerThread,
networkStack: NetworkStackClientBase,
nac: NetworkAgentConfig,
@@ -78,7 +98,7 @@
nmCbCaptor.capture())
// Create the actual agent. NetworkAgent is abstract, so make an anonymous subclass.
- if (SdkLevel.isAtLeastS()) {
+ if (deps.isAtLeastS()) {
agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
nc, lp, score.value, nac, provider) {}
} else {
@@ -92,7 +112,7 @@
}
private fun onValidationRequested() {
- if (SdkLevel.isAtLeastT()) {
+ if (deps.isAtLeastT()) {
verify(networkMonitor).notifyNetworkConnectedParcel(any())
} else {
verify(networkMonitor).notifyNetworkConnected(any(), any())
@@ -109,9 +129,10 @@
fun connect() {
val mgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
- val request = NetworkRequest.Builder().clearCapabilities()
- .addTransportType(nc.transportTypes[0])
- .build()
+ val request = NetworkRequest.Builder().apply {
+ clearCapabilities()
+ if (nc.transportTypes.isNotEmpty()) addTransportType(nc.transportTypes[0])
+ }.build()
val cb = TestableNetworkCallback()
mgr.registerNetworkCallback(request, cb)
agent.markConnected()
@@ -133,6 +154,15 @@
}
fun disconnect() {
+ val mgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val request = NetworkRequest.Builder().apply {
+ clearCapabilities()
+ if (nc.transportTypes.isNotEmpty()) addTransportType(nc.transportTypes[0])
+ }.build()
+ val cb = TestableNetworkCallback(timeoutMs = SHORT_TIMEOUT_MS)
+ mgr.registerNetworkCallback(request, cb)
+ cb.eventuallyExpect<Available> { it.network == agent.network }
agent.unregister()
+ cb.eventuallyExpect<Lost> { it.network == agent.network }
}
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index b11878d..2f78212 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
package com.android.server
import android.content.BroadcastReceiver
@@ -10,19 +26,20 @@
import android.net.ConnectivityManager
import android.net.INetd
import android.net.InetAddresses
-import android.net.IpPrefix
-import android.net.LinkAddress
import android.net.LinkProperties
import android.net.NetworkAgentConfig
import android.net.NetworkCapabilities
-import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
-import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkPolicyManager
import android.net.NetworkProvider
import android.net.NetworkScore
import android.net.PacProxyManager
-import android.net.RouteInfo
import android.net.networkstack.NetworkStackClientBase
import android.os.BatteryStatsManager
import android.os.Handler
@@ -60,6 +77,25 @@
open class FromS<Type>(val value: Type)
+internal const val VERSION_UNMOCKED = -1
+internal const val VERSION_R = 1
+internal const val VERSION_S = 2
+internal const val VERSION_T = 3
+internal const val VERSION_U = 4
+internal const val VERSION_V = 5
+internal const val VERSION_MAX = VERSION_V
+
+private fun NetworkCapabilities.getLegacyType() =
+ when (transportTypes.getOrElse(0) { TRANSPORT_WIFI }) {
+ TRANSPORT_BLUETOOTH -> ConnectivityManager.TYPE_BLUETOOTH
+ TRANSPORT_CELLULAR -> ConnectivityManager.TYPE_MOBILE
+ TRANSPORT_ETHERNET -> ConnectivityManager.TYPE_ETHERNET
+ TRANSPORT_TEST -> ConnectivityManager.TYPE_TEST
+ TRANSPORT_VPN -> ConnectivityManager.TYPE_VPN
+ TRANSPORT_WIFI -> ConnectivityManager.TYPE_WIFI
+ else -> ConnectivityManager.TYPE_NONE
+ }
+
/**
* Base class for tests testing ConnectivityService and its satellites.
*
@@ -108,9 +144,10 @@
val networkStack = mock<NetworkStackClientBase>()
val csHandlerThread = HandlerThread("CSTestHandler")
val sysResources = mock<Resources>().also { initMockedResources(it) }
- val packageManager = makeMockPackageManager()
+ val packageManager = makeMockPackageManager(instrumentationContext)
val connResources = makeMockConnResources(sysResources, packageManager)
+ val netd = mock<INetd>()
val bpfNetMaps = mock<BpfNetMaps>()
val clatCoordinator = mock<ClatCoordinator>()
val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
@@ -122,7 +159,7 @@
}
val deps = CSDeps()
- val service = makeConnectivityService(context, deps).also { it.systemReadyInternal() }
+ val service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
val cm = ConnectivityManager(context, service)
val csHandler = Handler(csHandlerThread.looper)
@@ -176,6 +213,24 @@
changeId in enabledChangeIds
override fun isChangeEnabled(changeId: Long, uid: Int) =
changeId in enabledChangeIds
+
+ // In AOSP, build version codes can't always distinguish between some versions (e.g. at the
+ // time of this writing U == V). Define custom ones.
+ private var sdkLevel = VERSION_UNMOCKED
+ private val isSdkUnmocked get() = sdkLevel == VERSION_UNMOCKED
+
+ fun setBuildSdk(sdkLevel: Int) {
+ require(sdkLevel <= VERSION_MAX) {
+ "setBuildSdk must not be called with Build.VERSION constants but " +
+ "CsTest.VERSION_* constants"
+ }
+ visibleOnHandlerThread(csHandler) { this.sdkLevel = sdkLevel }
+ }
+
+ override fun isAtLeastS() = if (isSdkUnmocked) super.isAtLeastS() else sdkLevel >= VERSION_S
+ override fun isAtLeastT() = if (isSdkUnmocked) super.isAtLeastT() else sdkLevel >= VERSION_T
+ override fun isAtLeastU() = if (isSdkUnmocked) super.isAtLeastU() else sdkLevel >= VERSION_U
+ override fun isAtLeastV() = if (isSdkUnmocked) super.isAtLeastV() else sdkLevel >= VERSION_V
}
inner class CSContext(base: Context) : BroadcastInterceptingContext(base) {
@@ -231,28 +286,15 @@
// Utility methods for subclasses to use
fun waitForIdle() = csHandlerThread.waitForIdle(HANDLER_TIMEOUT_MS)
- private fun emptyAgentConfig() = NetworkAgentConfig.Builder().build()
- private fun defaultNc() = NetworkCapabilities.Builder()
- // Add sensible defaults for agents that don't want to care
- .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
- .addCapability(NET_CAPABILITY_NOT_ROAMING)
- .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
- .build()
- private fun defaultScore() = FromS<NetworkScore>(NetworkScore.Builder().build())
- private fun defaultLp() = LinkProperties().apply {
- addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
- addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
- }
-
// Network agents. See CSAgentWrapper. This class contains utility methods to simplify
// creation.
fun Agent(
- nac: NetworkAgentConfig = emptyAgentConfig(),
nc: NetworkCapabilities = defaultNc(),
+ nac: NetworkAgentConfig = emptyAgentConfig(nc.getLegacyType()),
lp: LinkProperties = defaultLp(),
score: FromS<NetworkScore> = defaultScore(),
provider: NetworkProvider? = null
- ) = CSAgentWrapper(context, csHandlerThread, networkStack, nac, nc, lp, score, provider)
+ ) = CSAgentWrapper(context, deps, csHandlerThread, networkStack, nac, nc, lp, score, provider)
fun Agent(vararg transports: Int, lp: LinkProperties = defaultLp()): CSAgentWrapper {
val nc = NetworkCapabilities.Builder().apply {
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
index b8f2151..c1828b2 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
@file:JvmName("CsTestHelpers")
package com.android.server
@@ -14,7 +30,15 @@
import android.content.res.Resources
import android.net.IDnsResolver
import android.net.INetd
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkScore
+import android.net.RouteInfo
import android.net.metrics.IpConnectivityLog
+import android.os.Binder
import android.os.Handler
import android.os.HandlerThread
import android.os.SystemClock
@@ -31,19 +55,38 @@
import com.android.server.connectivity.ConnectivityResources
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.argThat
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito
import org.mockito.Mockito.doAnswer
-import org.mockito.Mockito.doReturn
import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
import kotlin.test.fail
internal inline fun <reified T> mock() = Mockito.mock(T::class.java)
internal inline fun <reified T> any() = any(T::class.java)
+internal fun emptyAgentConfig(legacyType: Int) = NetworkAgentConfig.Builder()
+ .setLegacyType(legacyType)
+ .build()
+
+internal fun defaultNc() = NetworkCapabilities.Builder()
+ // Add sensible defaults for agents that don't want to care
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+
+internal fun defaultScore() = FromS(NetworkScore.Builder().build())
+
+internal fun defaultLp() = LinkProperties().apply {
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+ addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+}
+
internal fun makeMockContentResolver(context: Context) = MockContentResolver(context).apply {
addProvider(Settings.AUTHORITY, FakeSettingsProvider())
}
@@ -59,9 +102,22 @@
}
}
-internal fun makeMockPackageManager() = mock<PackageManager>().also { pm ->
+internal fun makeMockPackageManager(realContext: Context) = mock<PackageManager>().also { pm ->
val supported = listOf(FEATURE_WIFI, FEATURE_WIFI_DIRECT, FEATURE_BLUETOOTH, FEATURE_ETHERNET)
doReturn(true).`when`(pm).hasSystemFeature(argThat { supported.contains(it) })
+ val myPackageName = realContext.packageName
+ val myPackageInfo = realContext.packageManager.getPackageInfo(myPackageName,
+ PackageManager.GET_PERMISSIONS)
+ // Very high version code so that the checks for the module version will always
+ // say that it is recent enough. This is the most sensible default, but if some
+ // test needs to test with different version codes they can re-mock this with a
+ // different value.
+ myPackageInfo.longVersionCode = 9999999L
+ doReturn(arrayOf(myPackageName)).`when`(pm).getPackagesForUid(Binder.getCallingUid())
+ doReturn(myPackageInfo).`when`(pm).getPackageInfoAsUser(
+ eq(myPackageName), anyInt(), eq(UserHandle.getCallingUserId()))
+ doReturn(listOf(myPackageInfo)).`when`(pm)
+ .getInstalledPackagesAsUser(eq(PackageManager.GET_PERMISSIONS), anyInt())
}
internal fun makeMockConnResources(resources: Resources, pm: PackageManager) = mock<Context>().let {
@@ -129,12 +185,13 @@
private val TEST_LINGER_DELAY_MS = 400
private val TEST_NASCENT_DELAY_MS = 300
-internal fun makeConnectivityService(context: Context, deps: Dependencies) = ConnectivityService(
- context,
- mock<IDnsResolver>(),
- mock<IpConnectivityLog>(),
- mock<INetd>(),
- deps).also {
- it.mLingerDelayMs = TEST_LINGER_DELAY_MS
- it.mNascentDelayMs = TEST_NASCENT_DELAY_MS
-}
+internal fun makeConnectivityService(context: Context, netd: INetd, deps: Dependencies) =
+ ConnectivityService(
+ context,
+ mock<IDnsResolver>(),
+ mock<IpConnectivityLog>(),
+ netd,
+ deps).also {
+ it.mLingerDelayMs = TEST_LINGER_DELAY_MS
+ it.mNascentDelayMs = TEST_NASCENT_DELAY_MS
+ }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index 292f77e..c477b2c 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -59,6 +59,7 @@
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.HandlerUtils;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -139,6 +140,14 @@
mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
}
+ @After
+ public void tearDown() throws Exception {
+ if (mObserverHandlerThread != null) {
+ mObserverHandlerThread.quitSafely();
+ mObserverHandlerThread.join();
+ }
+ }
+
@Test
public void testRegister_thresholdTooLow_setsDefaultThreshold() throws Exception {
final long thresholdTooLowBytes = 1L;
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 96056c6..278798e 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -38,7 +38,7 @@
"compatibility-device-util-axt",
"ctstestrunner-axt",
"net-tests-utils",
- "truth-prebuilt",
+ "truth",
],
libs: [
"android.test.base",