blob: 296b9ac07b84ed94647ecd97f99e95ae9c801509 [file] [log] [blame]
Lorenzo Colittid260ef22018-01-24 17:35:07 +09001/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.connectivity;
18
Jeff Sharkeye0c29952018-02-20 17:24:55 -070019import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_HANDOVER;
20import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY;
21import static android.net.ConnectivityManager.TYPE_MOBILE;
22import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
23import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
24
25import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH;
26
Lorenzo Colittid260ef22018-01-24 17:35:07 +090027import android.app.usage.NetworkStatsManager;
28import android.app.usage.NetworkStatsManager.UsageCallback;
29import android.content.Context;
Lorenzo Colittid260ef22018-01-24 17:35:07 +090030import android.net.ConnectivityManager;
31import android.net.ConnectivityManager.NetworkCallback;
32import android.net.Network;
33import android.net.NetworkCapabilities;
34import android.net.NetworkPolicyManager;
35import android.net.NetworkRequest;
36import android.net.NetworkStats;
37import android.net.NetworkTemplate;
38import android.net.StringNetworkSpecifier;
39import android.os.Handler;
Lorenzo Colittid260ef22018-01-24 17:35:07 +090040import android.telephony.TelephonyManager;
41import android.util.DebugUtils;
42import android.util.Slog;
43
Lorenzo Colittid260ef22018-01-24 17:35:07 +090044import com.android.internal.util.IndentingPrintWriter;
45import com.android.server.LocalServices;
46import com.android.server.net.NetworkPolicyManagerInternal;
Jeff Sharkeye0c29952018-02-20 17:24:55 -070047import com.android.server.net.NetworkStatsManagerInternal;
Lorenzo Colittid260ef22018-01-24 17:35:07 +090048
Jeff Sharkeye0c29952018-02-20 17:24:55 -070049import java.util.Calendar;
50import java.util.concurrent.ConcurrentHashMap;
Lorenzo Colittid260ef22018-01-24 17:35:07 +090051
52/**
53 * Manages multipath data budgets.
54 *
55 * Informs the return value of ConnectivityManager#getMultipathPreference() based on:
56 * - The user's data plan, as returned by getSubscriptionOpportunisticQuota().
57 * - The amount of data usage that occurs on mobile networks while they are not the system default
58 * network (i.e., when the app explicitly selected such networks).
59 *
60 * Currently, quota is determined on a daily basis, from midnight to midnight local time.
61 *
62 * @hide
63 */
64public class MultipathPolicyTracker {
65 private static String TAG = MultipathPolicyTracker.class.getSimpleName();
66
67 private static final boolean DBG = false;
68
69 private final Context mContext;
70 private final Handler mHandler;
71
72 private ConnectivityManager mCM;
Lorenzo Colittid260ef22018-01-24 17:35:07 +090073 private NetworkPolicyManager mNPM;
Jeff Sharkeye0c29952018-02-20 17:24:55 -070074 private NetworkStatsManager mStatsManager;
Lorenzo Colittid260ef22018-01-24 17:35:07 +090075
76 private NetworkCallback mMobileNetworkCallback;
77 private NetworkPolicyManager.Listener mPolicyListener;
78
79 // STOPSHIP: replace this with a configurable mechanism.
80 private static final long DEFAULT_DAILY_MULTIPATH_QUOTA = 2_500_000;
81
Lorenzo Colittid260ef22018-01-24 17:35:07 +090082 public MultipathPolicyTracker(Context ctx, Handler handler) {
83 mContext = ctx;
84 mHandler = handler;
85 // Because we are initialized by the ConnectivityService constructor, we can't touch any
86 // connectivity APIs. Service initialization is done in start().
87 }
88
89 public void start() {
Jeff Sharkeye0c29952018-02-20 17:24:55 -070090 mCM = mContext.getSystemService(ConnectivityManager.class);
91 mNPM = mContext.getSystemService(NetworkPolicyManager.class);
92 mStatsManager = mContext.getSystemService(NetworkStatsManager.class);
Lorenzo Colittid260ef22018-01-24 17:35:07 +090093
94 registerTrackMobileCallback();
95 registerNetworkPolicyListener();
96 }
97
98 public void shutdown() {
99 maybeUnregisterTrackMobileCallback();
100 unregisterNetworkPolicyListener();
101 for (MultipathTracker t : mMultipathTrackers.values()) {
102 t.shutdown();
103 }
104 mMultipathTrackers.clear();
105 }
106
107 // Called on an arbitrary binder thread.
108 public Integer getMultipathPreference(Network network) {
Jeff Sharkeye0c29952018-02-20 17:24:55 -0700109 if (network == null) {
110 return null;
111 }
Lorenzo Colittid260ef22018-01-24 17:35:07 +0900112 MultipathTracker t = mMultipathTrackers.get(network);
113 if (t != null) {
114 return t.getMultipathPreference();
115 }
116 return null;
117 }
118
119 // Track information on mobile networks as they come and go.
120 class MultipathTracker {
121 final Network network;
122 final int subId;
123 final String subscriberId;
124
125 private long mQuota;
126 /** Current multipath budget. Nonzero iff we have budget and a UsageCallback is armed. */
127 private long mMultipathBudget;
128 private final NetworkTemplate mNetworkTemplate;
129 private final UsageCallback mUsageCallback;
130
131 public MultipathTracker(Network network, NetworkCapabilities nc) {
132 this.network = network;
133 try {
134 subId = Integer.parseInt(
135 ((StringNetworkSpecifier) nc.getNetworkSpecifier()).toString());
136 } catch (ClassCastException | NullPointerException | NumberFormatException e) {
137 throw new IllegalStateException(String.format(
138 "Can't get subId from mobile network %s (%s): %s",
139 network, nc, e.getMessage()));
140 }
141
Jeff Sharkeye0c29952018-02-20 17:24:55 -0700142 TelephonyManager tele = mContext.getSystemService(TelephonyManager.class);
Lorenzo Colittid260ef22018-01-24 17:35:07 +0900143 if (tele == null) {
144 throw new IllegalStateException(String.format("Missing TelephonyManager"));
145 }
146 tele = tele.createForSubscriptionId(subId);
147 if (tele == null) {
148 throw new IllegalStateException(String.format(
149 "Can't get TelephonyManager for subId %d", subId));
150 }
151
152 subscriberId = tele.getSubscriberId();
153 mNetworkTemplate = new NetworkTemplate(
Jeff Sharkeye0c29952018-02-20 17:24:55 -0700154 NetworkTemplate.MATCH_MOBILE, subscriberId, new String[] { subscriberId },
Lorenzo Colittid260ef22018-01-24 17:35:07 +0900155 null, NetworkStats.METERED_ALL, NetworkStats.ROAMING_ALL,
156 NetworkStats.DEFAULT_NETWORK_NO);
157 mUsageCallback = new UsageCallback() {
158 @Override
159 public void onThresholdReached(int networkType, String subscriberId) {
160 if (DBG) Slog.d(TAG, "onThresholdReached for network " + network);
161 mMultipathBudget = 0;
162 updateMultipathBudget();
163 }
164 };
165
166 updateMultipathBudget();
167 }
168
169 private long getDailyNonDefaultDataUsage() {
170 Calendar start = Calendar.getInstance();
171 Calendar end = (Calendar) start.clone();
172 start.set(Calendar.HOUR_OF_DAY, 0);
173 start.set(Calendar.MINUTE, 0);
174 start.set(Calendar.SECOND, 0);
175 start.set(Calendar.MILLISECOND, 0);
176
Lorenzo Colittid260ef22018-01-24 17:35:07 +0900177 try {
Jeff Sharkeye0c29952018-02-20 17:24:55 -0700178 final long bytes = LocalServices.getService(NetworkStatsManagerInternal.class)
179 .getNetworkTotalBytes(mNetworkTemplate, start.getTimeInMillis(),
180 end.getTimeInMillis());
181 if (DBG) Slog.d(TAG, "Non-default data usage: " + bytes);
182 return bytes;
183 } catch (RuntimeException e) {
184 Slog.w(TAG, "Failed to get data usage: " + e);
185 return -1;
Lorenzo Colittid260ef22018-01-24 17:35:07 +0900186 }
Lorenzo Colittid260ef22018-01-24 17:35:07 +0900187 }
188
189 void updateMultipathBudget() {
Jeff Sharkeye0c29952018-02-20 17:24:55 -0700190 long quota = LocalServices.getService(NetworkPolicyManagerInternal.class)
191 .getSubscriptionOpportunisticQuota(this.network, QUOTA_TYPE_MULTIPATH);
Lorenzo Colittid260ef22018-01-24 17:35:07 +0900192 if (DBG) Slog.d(TAG, "Opportunistic quota from data plan: " + quota + " bytes");
193
194 if (quota == 0) {
195 // STOPSHIP: replace this with a configurable mechanism.
196 quota = DEFAULT_DAILY_MULTIPATH_QUOTA;
197 if (DBG) Slog.d(TAG, "Setting quota: " + quota + " bytes");
198 }
199
200 if (haveMultipathBudget() && quota == mQuota) {
201 // If we already have a usage callback pending , there's no need to re-register it
202 // if the quota hasn't changed. The callback will simply fire as expected when the
203 // budget is spent. Also: if we re-register the callback when we're below the
204 // UsageCallback's minimum value of 2MB, we'll overshoot the budget.
205 if (DBG) Slog.d(TAG, "Quota still " + quota + ", not updating.");
206 return;
207 }
208 mQuota = quota;
209
Jeff Sharkeye0c29952018-02-20 17:24:55 -0700210 // If we can't get current usage, assume the worst and don't give
211 // ourselves any budget to work with.
212 final long usage = getDailyNonDefaultDataUsage();
213 final long budget = (usage == -1) ? 0 : Math.max(0, quota - usage);
Lorenzo Colittid260ef22018-01-24 17:35:07 +0900214 if (budget > 0) {
215 if (DBG) Slog.d(TAG, "Setting callback for " + budget +
216 " bytes on network " + network);
217 registerUsageCallback(budget);
218 } else {
219 maybeUnregisterUsageCallback();
220 }
221 }
222
223 public int getMultipathPreference() {
224 if (haveMultipathBudget()) {
225 return MULTIPATH_PREFERENCE_HANDOVER | MULTIPATH_PREFERENCE_RELIABILITY;
226 }
227 return 0;
228 }
229
230 // For debugging only.
231 public long getQuota() {
232 return mQuota;
233 }
234
235 // For debugging only.
236 public long getMultipathBudget() {
237 return mMultipathBudget;
238 }
239
240 private boolean haveMultipathBudget() {
241 return mMultipathBudget > 0;
242 }
243
244 private void registerUsageCallback(long budget) {
245 maybeUnregisterUsageCallback();
246 mStatsManager.registerUsageCallback(mNetworkTemplate, TYPE_MOBILE, budget,
247 mUsageCallback, mHandler);
248 mMultipathBudget = budget;
249 }
250
251 private void maybeUnregisterUsageCallback() {
252 if (haveMultipathBudget()) {
253 if (DBG) Slog.d(TAG, "Unregistering callback, budget was " + mMultipathBudget);
254 mStatsManager.unregisterUsageCallback(mUsageCallback);
255 mMultipathBudget = 0;
256 }
257 }
258
259 void shutdown() {
260 maybeUnregisterUsageCallback();
261 }
262 }
263
264 // Only ever updated on the handler thread. Accessed from other binder threads to retrieve
265 // the tracker for a specific network.
266 private final ConcurrentHashMap <Network, MultipathTracker> mMultipathTrackers =
267 new ConcurrentHashMap<>();
268
269 // TODO: this races with app code that might respond to onAvailable() by immediately calling
270 // getMultipathPreference. Fix this by adding to ConnectivityService the ability to directly
271 // invoke NetworkCallbacks on tightly-coupled classes such as this one which run on its
272 // handler thread.
273 private void registerTrackMobileCallback() {
274 final NetworkRequest request = new NetworkRequest.Builder()
275 .addCapability(NET_CAPABILITY_INTERNET)
276 .addTransportType(TRANSPORT_CELLULAR)
277 .build();
278 mMobileNetworkCallback = new ConnectivityManager.NetworkCallback() {
279 @Override
280 public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
281 MultipathTracker existing = mMultipathTrackers.get(network);
282 if (existing != null) {
283 existing.updateMultipathBudget();
284 return;
285 }
286
287 try {
288 mMultipathTrackers.put(network, new MultipathTracker(network, nc));
289 } catch (IllegalStateException e) {
290 Slog.e(TAG, "Can't track mobile network " + network + ": " + e.getMessage());
291 }
292 if (DBG) Slog.d(TAG, "Tracking mobile network " + network);
293 }
294
295 @Override
296 public void onLost(Network network) {
297 MultipathTracker existing = mMultipathTrackers.get(network);
298 if (existing != null) {
299 existing.shutdown();
300 mMultipathTrackers.remove(network);
301 }
302 if (DBG) Slog.d(TAG, "No longer tracking mobile network " + network);
303 }
304 };
305
306 mCM.registerNetworkCallback(request, mMobileNetworkCallback, mHandler);
307 }
308
309 private void maybeUnregisterTrackMobileCallback() {
310 if (mMobileNetworkCallback != null) {
311 mCM.unregisterNetworkCallback(mMobileNetworkCallback);
312 }
313 mMobileNetworkCallback = null;
314 }
315
316 private void registerNetworkPolicyListener() {
317 mPolicyListener = new NetworkPolicyManager.Listener() {
318 @Override
319 public void onMeteredIfacesChanged(String[] meteredIfaces) {
320 // Dispatched every time opportunistic quota is recalculated.
321 mHandler.post(() -> {
322 for (MultipathTracker t : mMultipathTrackers.values()) {
323 t.updateMultipathBudget();
324 }
325 });
326 }
327 };
328 mNPM.registerListener(mPolicyListener);
329 }
330
331 private void unregisterNetworkPolicyListener() {
332 mNPM.unregisterListener(mPolicyListener);
333 }
334
335 public void dump(IndentingPrintWriter pw) {
336 // Do not use in production. Access to class data is only safe on the handler thrad.
337 pw.println("MultipathPolicyTracker:");
338 pw.increaseIndent();
339 for (MultipathTracker t : mMultipathTrackers.values()) {
340 pw.println(String.format("Network %s: quota %d, budget %d. Preference: %s",
341 t.network, t.getQuota(), t.getMultipathBudget(),
342 DebugUtils.flagsToString(ConnectivityManager.class, "MULTIPATH_PREFERENCE_",
343 t.getMultipathPreference())));
344 }
345 pw.decreaseIndent();
346 }
347}