1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
|
/*
* Copyright (C) 2020 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.networkstack.tethering;
import static android.net.TetheringManager.TETHERING_WIFI;
import android.net.MacAddress;
import android.net.TetheredClient;
import android.net.TetheredClient.AddressInfo;
import android.net.ip.IpServer;
import android.net.wifi.WifiClient;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Tracker for clients connected to downstreams.
*
* <p>This class is not thread safe, it is intended to be used only from the tethering handler
* thread.
*/
public class ConnectedClientsTracker {
private final Clock mClock;
@NonNull
private List<WifiClient> mLastWifiClients = Collections.emptyList();
@NonNull
private List<TetheredClient> mLastTetheredClients = Collections.emptyList();
@VisibleForTesting
static class Clock {
public long elapsedRealtime() {
return SystemClock.elapsedRealtime();
}
}
public ConnectedClientsTracker() {
this(new Clock());
}
@VisibleForTesting
ConnectedClientsTracker(Clock clock) {
mClock = clock;
}
/**
* Update the tracker with new connected clients.
*
* <p>The new list can be obtained through {@link #getLastTetheredClients()}.
* @param ipServers The IpServers used to assign addresses to clients.
* @param wifiClients The list of L2-connected WiFi clients. Null for no change since last
* update.
* @return True if the list of clients changed since the last calculation.
*/
public boolean updateConnectedClients(
Iterable<IpServer> ipServers, @Nullable List<WifiClient> wifiClients) {
final long now = mClock.elapsedRealtime();
if (wifiClients != null) {
mLastWifiClients = wifiClients;
}
final Set<MacAddress> wifiClientMacs = getClientMacs(mLastWifiClients);
// Build the list of non-expired leases from all IpServers, grouped by mac address
final Map<MacAddress, TetheredClient> clientsMap = new HashMap<>();
for (IpServer server : ipServers) {
for (TetheredClient client : server.getAllLeases()) {
if (client.getTetheringType() == TETHERING_WIFI
&& !wifiClientMacs.contains(client.getMacAddress())) {
// Skip leases of WiFi clients that are not (or no longer) L2-connected
continue;
}
final TetheredClient prunedClient = pruneExpired(client, now);
if (prunedClient == null) continue; // All addresses expired
addLease(clientsMap, prunedClient);
}
}
// TODO: add IPv6 addresses from netlink
// Add connected WiFi clients that do not have any known address
for (MacAddress client : wifiClientMacs) {
if (clientsMap.containsKey(client)) continue;
clientsMap.put(client, new TetheredClient(
client, Collections.emptyList() /* addresses */, TETHERING_WIFI));
}
final HashSet<TetheredClient> clients = new HashSet<>(clientsMap.values());
final boolean clientsChanged = clients.size() != mLastTetheredClients.size()
|| !clients.containsAll(mLastTetheredClients);
mLastTetheredClients = Collections.unmodifiableList(new ArrayList<>(clients));
return clientsChanged;
}
private static void addLease(Map<MacAddress, TetheredClient> clientsMap, TetheredClient lease) {
final TetheredClient aggregateClient = clientsMap.getOrDefault(
lease.getMacAddress(), lease);
if (aggregateClient == lease) {
// This is the first lease with this mac address
clientsMap.put(lease.getMacAddress(), lease);
return;
}
// Only add the address info; this assumes that the tethering type is the same when the mac
// address is the same. If a client is connected through different tethering types with the
// same mac address, connected clients callbacks will report all of its addresses under only
// one of these tethering types. This keeps the API simple considering that such a scenario
// would really be a rare edge case.
clientsMap.put(lease.getMacAddress(), aggregateClient.addAddresses(lease));
}
/**
* Get the last list of tethered clients, as calculated in {@link #updateConnectedClients}.
*
* <p>The returned list is immutable.
*/
@NonNull
public List<TetheredClient> getLastTetheredClients() {
return mLastTetheredClients;
}
private static boolean hasExpiredAddress(List<AddressInfo> addresses, long now) {
for (AddressInfo info : addresses) {
if (info.getExpirationTime() <= now) {
return true;
}
}
return false;
}
@Nullable
private static TetheredClient pruneExpired(TetheredClient client, long now) {
final List<AddressInfo> addresses = client.getAddresses();
if (addresses.size() == 0) return null;
if (!hasExpiredAddress(addresses, now)) return client;
final ArrayList<AddressInfo> newAddrs = new ArrayList<>(addresses.size() - 1);
for (AddressInfo info : addresses) {
if (info.getExpirationTime() > now) {
newAddrs.add(info);
}
}
if (newAddrs.size() == 0) {
return null;
}
return new TetheredClient(client.getMacAddress(), newAddrs, client.getTetheringType());
}
@NonNull
private static Set<MacAddress> getClientMacs(@NonNull List<WifiClient> clients) {
final Set<MacAddress> macs = new HashSet<>(clients.size());
for (WifiClient c : clients) {
macs.add(c.getMacAddress());
}
return macs;
}
}
|