1 /*
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 
17 package com.android.server.connectivity;
18 
19 import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_HANDOVER;
20 import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY;
21 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
22 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
23 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
24 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
25 import static android.net.NetworkPolicy.LIMIT_DISABLED;
26 import static android.net.NetworkPolicy.WARNING_DISABLED;
27 import static android.provider.Settings.Global.NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES;
28 
29 import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH;
30 import static com.android.server.net.NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN;
31 
32 import android.app.usage.NetworkStatsManager;
33 import android.app.usage.NetworkStatsManager.UsageCallback;
34 import android.content.BroadcastReceiver;
35 import android.content.ContentResolver;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.IntentFilter;
39 import android.database.ContentObserver;
40 import android.net.ConnectivityManager;
41 import android.net.ConnectivityManager.NetworkCallback;
42 import android.net.Network;
43 import android.net.NetworkCapabilities;
44 import android.net.NetworkIdentity;
45 import android.net.NetworkPolicy;
46 import android.net.NetworkPolicyManager;
47 import android.net.NetworkRequest;
48 import android.net.NetworkSpecifier;
49 import android.net.NetworkStats;
50 import android.net.NetworkTemplate;
51 import android.net.TelephonyNetworkSpecifier;
52 import android.net.Uri;
53 import android.os.BestClock;
54 import android.os.Handler;
55 import android.os.SystemClock;
56 import android.os.UserHandle;
57 import android.provider.Settings;
58 import android.telephony.TelephonyManager;
59 import android.util.DebugUtils;
60 import android.util.Log;
61 import android.util.Range;
62 
63 import com.android.internal.R;
64 import com.android.internal.annotations.VisibleForTesting;
65 import com.android.internal.util.IndentingPrintWriter;
66 import com.android.server.LocalServices;
67 import com.android.server.net.NetworkPolicyManagerInternal;
68 
69 import java.time.Clock;
70 import java.time.ZoneId;
71 import java.time.ZoneOffset;
72 import java.time.ZonedDateTime;
73 import java.time.temporal.ChronoUnit;
74 import java.util.Set;
75 import java.util.concurrent.ConcurrentHashMap;
76 import java.util.concurrent.TimeUnit;
77 
78 /**
79  * Manages multipath data budgets.
80  *
81  * Informs the return value of ConnectivityManager#getMultipathPreference() based on:
82  * - The user's data plan, as returned by getSubscriptionOpportunisticQuota().
83  * - The amount of data usage that occurs on mobile networks while they are not the system default
84  *   network (i.e., when the app explicitly selected such networks).
85  *
86  * Currently, quota is determined on a daily basis, from midnight to midnight local time.
87  *
88  * @hide
89  */
90 public class MultipathPolicyTracker {
91     private static String TAG = MultipathPolicyTracker.class.getSimpleName();
92 
93     private static final boolean DBG = false;
94     private static final long MIN_THRESHOLD_BYTES = 2 * 1_048_576L; // 2MiB
95 
96     // This context is for the current user.
97     private final Context mContext;
98     // This context is for all users, so register a BroadcastReceiver which can receive intents from
99     // all users.
100     private final Context mUserAllContext;
101     private final Handler mHandler;
102     private final Clock mClock;
103     private final Dependencies mDeps;
104     private final ContentResolver mResolver;
105     private final ConfigChangeReceiver mConfigChangeReceiver;
106 
107     @VisibleForTesting
108     final ContentObserver mSettingsObserver;
109 
110     private ConnectivityManager mCM;
111     private NetworkPolicyManager mNPM;
112     private NetworkStatsManager mStatsManager;
113 
114     private NetworkCallback mMobileNetworkCallback;
115     private NetworkPolicyManager.Listener mPolicyListener;
116 
117 
118     /**
119      * Divider to calculate opportunistic quota from user-set data limit or warning: 5% of user-set
120      * limit.
121      */
122     private static final int OPQUOTA_USER_SETTING_DIVIDER = 20;
123 
124     public static class Dependencies {
getClock()125         public Clock getClock() {
126             return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
127                     Clock.systemUTC());
128         }
129     }
130 
MultipathPolicyTracker(Context ctx, Handler handler)131     public MultipathPolicyTracker(Context ctx, Handler handler) {
132         this(ctx, handler, new Dependencies());
133     }
134 
MultipathPolicyTracker(Context ctx, Handler handler, Dependencies deps)135     public MultipathPolicyTracker(Context ctx, Handler handler, Dependencies deps) {
136         mContext = ctx;
137         mUserAllContext = ctx.createContextAsUser(UserHandle.ALL, 0 /* flags */);
138         mHandler = handler;
139         mClock = deps.getClock();
140         mDeps = deps;
141         mResolver = mContext.getContentResolver();
142         mSettingsObserver = new SettingsObserver(mHandler);
143         mConfigChangeReceiver = new ConfigChangeReceiver();
144         // Because we are initialized by the ConnectivityService constructor, we can't touch any
145         // connectivity APIs. Service initialization is done in start().
146     }
147 
start()148     public void start() {
149         mCM = mContext.getSystemService(ConnectivityManager.class);
150         mNPM = mContext.getSystemService(NetworkPolicyManager.class);
151         mStatsManager = mContext.getSystemService(NetworkStatsManager.class);
152 
153         registerTrackMobileCallback();
154         registerNetworkPolicyListener();
155         final Uri defaultSettingUri =
156                 Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES);
157         mResolver.registerContentObserver(defaultSettingUri, false, mSettingsObserver);
158 
159         final IntentFilter intentFilter = new IntentFilter();
160         intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
161         mUserAllContext.registerReceiver(
162                 mConfigChangeReceiver, intentFilter, null /* broadcastPermission */, mHandler);
163     }
164 
shutdown()165     public void shutdown() {
166         maybeUnregisterTrackMobileCallback();
167         unregisterNetworkPolicyListener();
168         for (MultipathTracker t : mMultipathTrackers.values()) {
169             t.shutdown();
170         }
171         mMultipathTrackers.clear();
172         mResolver.unregisterContentObserver(mSettingsObserver);
173         mUserAllContext.unregisterReceiver(mConfigChangeReceiver);
174     }
175 
176     // Called on an arbitrary binder thread.
getMultipathPreference(Network network)177     public Integer getMultipathPreference(Network network) {
178         if (network == null) {
179             return null;
180         }
181         MultipathTracker t = mMultipathTrackers.get(network);
182         if (t != null) {
183             return t.getMultipathPreference();
184         }
185         return null;
186     }
187 
188     // Track information on mobile networks as they come and go.
189     class MultipathTracker {
190         final Network network;
191         final String subscriberId;
192         private final int mSubId;
193 
194         private long mQuota;
195         /** Current multipath budget. Nonzero iff we have budget. */
196         // The budget could be accessed by multiple threads, make it volatile to ensure the callers
197         // on a different thread will not see the stale value.
198         private volatile long mMultipathBudget;
199         private final NetworkTemplate mNetworkTemplate;
200         private final UsageCallback mUsageCallback;
201         private boolean mUsageCallbackRegistered = false;
202         private NetworkCapabilities mNetworkCapabilities;
203         private final NetworkStatsManager mStatsManager;
204 
MultipathTracker(Network network, NetworkCapabilities nc)205         public MultipathTracker(Network network, NetworkCapabilities nc) {
206             this.network = network;
207             this.mNetworkCapabilities = new NetworkCapabilities(nc);
208             NetworkSpecifier specifier = nc.getNetworkSpecifier();
209             if (specifier instanceof TelephonyNetworkSpecifier) {
210                 mSubId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
211             } else {
212                 throw new IllegalStateException(String.format(
213                         "Can't get subId from mobile network %s (%s)",
214                         network, nc));
215             }
216 
217             TelephonyManager tele = mContext.getSystemService(TelephonyManager.class);
218             if (tele == null) {
219                 throw new IllegalStateException(String.format("Missing TelephonyManager"));
220             }
221             tele = tele.createForSubscriptionId(mSubId);
222             if (tele == null) {
223                 throw new IllegalStateException(String.format(
224                         "Can't get TelephonyManager for subId %d", mSubId));
225             }
226 
227             subscriberId = tele.getSubscriberId();
228             if (subscriberId == null) {
229                 throw new IllegalStateException("Null subscriber Id for subId " + mSubId);
230             }
231             mNetworkTemplate = new NetworkTemplate.Builder(NetworkTemplate.MATCH_MOBILE)
232                     .setSubscriberIds(Set.of(subscriberId))
233                     .setMeteredness(NetworkStats.METERED_YES)
234                     .setDefaultNetworkStatus(NetworkStats.DEFAULT_NETWORK_NO)
235                     .build();
236             mUsageCallback = new UsageCallback() {
237                 @Override
238                 public void onThresholdReached(int networkType, String subscriberId) {
239                     if (DBG) Log.d(TAG, "onThresholdReached for network " + network);
240                     updateMultipathBudget();
241                 }
242             };
243             mStatsManager = mContext.getSystemService(NetworkStatsManager.class);
244             // Query stats from NetworkStatsService will trigger a poll by default.
245             // But since MultipathPolicyTracker listens NPMS events that triggered by
246             // stats updated event, and will query stats
247             // after the event. A polling -> updated -> query -> polling loop will be introduced
248             // if polls on open. Hence, set flag to false to prevent a polling loop.
249             mStatsManager.setPollOnOpen(false);
250 
251             updateMultipathBudget();
252         }
253 
setNetworkCapabilities(NetworkCapabilities nc)254         public void setNetworkCapabilities(NetworkCapabilities nc) {
255             mNetworkCapabilities = new NetworkCapabilities(nc);
256         }
257 
258         // TODO: calculate with proper timezone information
getDailyNonDefaultDataUsage()259         private long getDailyNonDefaultDataUsage() {
260             final ZonedDateTime end =
261                     ZonedDateTime.ofInstant(mClock.instant(), ZoneId.systemDefault());
262             final ZonedDateTime start = end.truncatedTo(ChronoUnit.DAYS);
263 
264             final long bytes = getNetworkTotalBytes(
265                     start.toInstant().toEpochMilli(),
266                     end.toInstant().toEpochMilli());
267             if (DBG) Log.d(TAG, "Non-default data usage: " + bytes);
268             return bytes;
269         }
270 
getNetworkTotalBytes(long start, long end)271         private long getNetworkTotalBytes(long start, long end) {
272             try {
273                 final android.app.usage.NetworkStats.Bucket ret =
274                         mStatsManager.querySummaryForDevice(mNetworkTemplate, start, end);
275                 return ret.getRxBytes() + ret.getTxBytes();
276             } catch (RuntimeException e) {
277                 Log.w(TAG, "Failed to get data usage: " + e);
278                 return -1;
279             }
280         }
281 
getTemplateMatchingNetworkIdentity(NetworkCapabilities nc)282         private NetworkIdentity getTemplateMatchingNetworkIdentity(NetworkCapabilities nc) {
283             return new NetworkIdentity.Builder().setType(ConnectivityManager.TYPE_MOBILE)
284                     .setSubscriberId(subscriberId)
285                     .setRoaming(!nc.hasCapability(NET_CAPABILITY_NOT_ROAMING))
286                     .setMetered(!nc.hasCapability(NET_CAPABILITY_NOT_METERED))
287                     .setSubId(mSubId)
288                     .build();
289         }
290 
getRemainingDailyBudget(long limitBytes, Range<ZonedDateTime> cycle)291         private long getRemainingDailyBudget(long limitBytes,
292                 Range<ZonedDateTime> cycle) {
293             final long start = cycle.getLower().toInstant().toEpochMilli();
294             final long end = cycle.getUpper().toInstant().toEpochMilli();
295             final long totalBytes = getNetworkTotalBytes(start, end);
296             final long remainingBytes = totalBytes == -1 ? 0 : Math.max(0, limitBytes - totalBytes);
297             // 1 + ((end - now - 1) / millisInDay with integers is equivalent to:
298             // ceil((double)(end - now) / millisInDay)
299             final long remainingDays =
300                     1 + ((end - mClock.millis() - 1) / TimeUnit.DAYS.toMillis(1));
301 
302             return remainingBytes / Math.max(1, remainingDays);
303         }
304 
getUserPolicyOpportunisticQuotaBytes()305         private long getUserPolicyOpportunisticQuotaBytes() {
306             // Keep the most restrictive applicable policy
307             long minQuota = Long.MAX_VALUE;
308             final NetworkIdentity identity = getTemplateMatchingNetworkIdentity(
309                     mNetworkCapabilities);
310 
311             final NetworkPolicy[] policies = mNPM.getNetworkPolicies();
312             for (NetworkPolicy policy : policies) {
313                 if (policy.hasCycle() && policy.template.matches(identity)) {
314                     final long cycleStart = policy.cycleIterator().next().getLower()
315                             .toInstant().toEpochMilli();
316                     // Prefer user-defined warning, otherwise use hard limit
317                     final long activeWarning = getActiveWarning(policy, cycleStart);
318                     final long policyBytes = (activeWarning == WARNING_DISABLED)
319                             ? getActiveLimit(policy, cycleStart)
320                             : activeWarning;
321 
322                     if (policyBytes != LIMIT_DISABLED && policyBytes != WARNING_DISABLED) {
323                         final long policyBudget = getRemainingDailyBudget(policyBytes,
324                                 policy.cycleIterator().next());
325                         minQuota = Math.min(minQuota, policyBudget);
326                     }
327                 }
328             }
329 
330             if (minQuota == Long.MAX_VALUE) {
331                 return OPPORTUNISTIC_QUOTA_UNKNOWN;
332             }
333 
334             return minQuota / OPQUOTA_USER_SETTING_DIVIDER;
335         }
336 
updateMultipathBudget()337         void updateMultipathBudget() {
338             long quota = LocalServices.getService(NetworkPolicyManagerInternal.class)
339                     .getSubscriptionOpportunisticQuota(this.network, QUOTA_TYPE_MULTIPATH);
340             if (DBG) Log.d(TAG, "Opportunistic quota from data plan: " + quota + " bytes");
341 
342             // Fallback to user settings-based quota if not available from phone plan
343             if (quota == OPPORTUNISTIC_QUOTA_UNKNOWN) {
344                 quota = getUserPolicyOpportunisticQuotaBytes();
345                 if (DBG) Log.d(TAG, "Opportunistic quota from user policy: " + quota + " bytes");
346             }
347 
348             if (quota == OPPORTUNISTIC_QUOTA_UNKNOWN) {
349                 quota = getDefaultDailyMultipathQuotaBytes();
350                 if (DBG) Log.d(TAG, "Setting quota: " + quota + " bytes");
351             }
352 
353             // TODO: re-register if day changed: budget may have run out but should be refreshed.
354             if (haveMultipathBudget() && quota == mQuota) {
355                 // If there is already a usage callback pending , there's no need to re-register it
356                 // if the quota hasn't changed. The callback will simply fire as expected when the
357                 // budget is spent.
358                 if (DBG) Log.d(TAG, "Quota still " + quota + ", not updating.");
359                 return;
360             }
361             mQuota = quota;
362 
363             // If we can't get current usage, assume the worst and don't give
364             // ourselves any budget to work with.
365             final long usage = getDailyNonDefaultDataUsage();
366             final long budget = (usage == -1) ? 0 : Math.max(0, quota - usage);
367 
368             // Only consider budgets greater than MIN_THRESHOLD_BYTES, otherwise the callback will
369             // fire late, after data usage went over budget. Also budget should be 0 if remaining
370             // data is close to 0.
371             // This is necessary because the usage callback does not accept smaller thresholds.
372             // Because it snaps everything to MIN_THRESHOLD_BYTES, the lesser of the two evils is
373             // to snap to 0 here.
374             // This will only be called if the total quota for the day changed, not if usage changed
375             // since last time, so even if this is called very often the budget will not snap to 0
376             // as soon as there are less than 2MB left for today.
377             if (budget > MIN_THRESHOLD_BYTES) {
378                 if (DBG) {
379                     Log.d(TAG, "Setting callback for " + budget + " bytes on network " + network);
380                 }
381                 setMultipathBudget(budget);
382             } else {
383                 clearMultipathBudget();
384             }
385         }
386 
getMultipathPreference()387         public int getMultipathPreference() {
388             if (haveMultipathBudget()) {
389                 return MULTIPATH_PREFERENCE_HANDOVER | MULTIPATH_PREFERENCE_RELIABILITY;
390             }
391             return 0;
392         }
393 
394         // For debugging only.
getQuota()395         public long getQuota() {
396             return mQuota;
397         }
398 
399         // For debugging only.
getMultipathBudget()400         public long getMultipathBudget() {
401             return mMultipathBudget;
402         }
403 
haveMultipathBudget()404         private boolean haveMultipathBudget() {
405             return mMultipathBudget > 0;
406         }
407 
408         // Sets the budget and registers a usage callback for it.
setMultipathBudget(long budget)409         private void setMultipathBudget(long budget) {
410             maybeUnregisterUsageCallback();
411             if (DBG) Log.d(TAG, "Registering callback, budget is " + mMultipathBudget);
412             mStatsManager.registerUsageCallback(mNetworkTemplate, budget,
413                     (command) -> mHandler.post(command), mUsageCallback);
414             mUsageCallbackRegistered = true;
415             mMultipathBudget = budget;
416         }
417 
maybeUnregisterUsageCallback()418         private void maybeUnregisterUsageCallback() {
419             if (!mUsageCallbackRegistered) return;
420             if (DBG) Log.d(TAG, "Unregistering callback, budget was " + mMultipathBudget);
421             mStatsManager.unregisterUsageCallback(mUsageCallback);
422             mUsageCallbackRegistered = false;
423         }
424 
clearMultipathBudget()425         private void clearMultipathBudget() {
426             maybeUnregisterUsageCallback();
427             mMultipathBudget = 0;
428         }
429 
shutdown()430         void shutdown() {
431             clearMultipathBudget();
432         }
433     }
434 
getActiveWarning(NetworkPolicy policy, long cycleStart)435     private static long getActiveWarning(NetworkPolicy policy, long cycleStart) {
436         return policy.lastWarningSnooze < cycleStart
437                 ? policy.warningBytes
438                 : WARNING_DISABLED;
439     }
440 
getActiveLimit(NetworkPolicy policy, long cycleStart)441     private static long getActiveLimit(NetworkPolicy policy, long cycleStart) {
442         return policy.lastLimitSnooze < cycleStart
443                 ? policy.limitBytes
444                 : LIMIT_DISABLED;
445     }
446 
447     // Only ever updated on the handler thread. Accessed from other binder threads to retrieve
448     // the tracker for a specific network.
449     private final ConcurrentHashMap <Network, MultipathTracker> mMultipathTrackers =
450             new ConcurrentHashMap<>();
451 
getDefaultDailyMultipathQuotaBytes()452     private long getDefaultDailyMultipathQuotaBytes() {
453         final String setting = Settings.Global.getString(mContext.getContentResolver(),
454                 NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES);
455         if (setting != null) {
456             try {
457                 return Long.parseLong(setting);
458             } catch(NumberFormatException e) {
459                 // fall through
460             }
461         }
462 
463         return mContext.getResources().getInteger(
464                 R.integer.config_networkDefaultDailyMultipathQuotaBytes);
465     }
466 
467     // TODO: this races with app code that might respond to onAvailable() by immediately calling
468     // getMultipathPreference. Fix this by adding to ConnectivityService the ability to directly
469     // invoke NetworkCallbacks on tightly-coupled classes such as this one which run on its
470     // handler thread.
registerTrackMobileCallback()471     private void registerTrackMobileCallback() {
472         final NetworkRequest request = new NetworkRequest.Builder()
473                 .addCapability(NET_CAPABILITY_INTERNET)
474                 .addTransportType(TRANSPORT_CELLULAR)
475                 .build();
476         mMobileNetworkCallback = new ConnectivityManager.NetworkCallback() {
477             @Override
478             public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
479                 MultipathTracker existing = mMultipathTrackers.get(network);
480                 if (existing != null) {
481                     existing.setNetworkCapabilities(nc);
482                     existing.updateMultipathBudget();
483                     return;
484                 }
485 
486                 try {
487                     mMultipathTrackers.put(network, new MultipathTracker(network, nc));
488                 } catch (IllegalStateException e) {
489                     Log.e(TAG, "Can't track mobile network " + network + ": " + e.getMessage());
490                 }
491                 if (DBG) Log.d(TAG, "Tracking mobile network " + network);
492             }
493 
494             @Override
495             public void onLost(Network network) {
496                 MultipathTracker existing = mMultipathTrackers.get(network);
497                 if (existing != null) {
498                     existing.shutdown();
499                     mMultipathTrackers.remove(network);
500                 }
501                 if (DBG) Log.d(TAG, "No longer tracking mobile network " + network);
502             }
503         };
504 
505         mCM.registerNetworkCallback(request, mMobileNetworkCallback, mHandler);
506     }
507 
508     /**
509      * Update multipath budgets for all trackers. To be called on the mHandler thread.
510      */
updateAllMultipathBudgets()511     private void updateAllMultipathBudgets() {
512         for (MultipathTracker t : mMultipathTrackers.values()) {
513             t.updateMultipathBudget();
514         }
515     }
516 
maybeUnregisterTrackMobileCallback()517     private void maybeUnregisterTrackMobileCallback() {
518         if (mMobileNetworkCallback != null) {
519             mCM.unregisterNetworkCallback(mMobileNetworkCallback);
520         }
521         mMobileNetworkCallback = null;
522     }
523 
registerNetworkPolicyListener()524     private void registerNetworkPolicyListener() {
525         mPolicyListener = new NetworkPolicyManager.Listener() {
526             @Override
527             public void onMeteredIfacesChanged(String[] meteredIfaces) {
528                 // Dispatched every time opportunistic quota is recalculated.
529                 mHandler.post(() -> updateAllMultipathBudgets());
530             }
531         };
532         mNPM.registerListener(mPolicyListener);
533     }
534 
unregisterNetworkPolicyListener()535     private void unregisterNetworkPolicyListener() {
536         mNPM.unregisterListener(mPolicyListener);
537     }
538 
539     private final class SettingsObserver extends ContentObserver {
SettingsObserver(Handler handler)540         public SettingsObserver(Handler handler) {
541             super(handler);
542         }
543 
544         @Override
onChange(boolean selfChange)545         public void onChange(boolean selfChange) {
546             Log.wtf(TAG, "Should never be reached.");
547         }
548 
549         @Override
onChange(boolean selfChange, Uri uri)550         public void onChange(boolean selfChange, Uri uri) {
551             if (!Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES)
552                     .equals(uri)) {
553                 Log.wtf(TAG, "Unexpected settings observation: " + uri);
554             }
555             if (DBG) Log.d(TAG, "Settings change: updating budgets.");
556             updateAllMultipathBudgets();
557         }
558     }
559 
560     private final class ConfigChangeReceiver extends BroadcastReceiver {
561         @Override
onReceive(Context context, Intent intent)562         public void onReceive(Context context, Intent intent) {
563             if (DBG) Log.d(TAG, "Configuration change: updating budgets.");
564             updateAllMultipathBudgets();
565         }
566     }
567 
dump(IndentingPrintWriter pw)568     public void dump(IndentingPrintWriter pw) {
569         // Do not use in production. Access to class data is only safe on the handler thrad.
570         pw.println("MultipathPolicyTracker:");
571         pw.increaseIndent();
572         for (MultipathTracker t : mMultipathTrackers.values()) {
573             pw.println(String.format("Network %s: quota %d, budget %d. Preference: %s",
574                     t.network, t.getQuota(), t.getMultipathBudget(),
575                     DebugUtils.flagsToString(ConnectivityManager.class, "MULTIPATH_PREFERENCE_",
576                             t.getMultipathPreference())));
577         }
578         pw.decreaseIndent();
579     }
580 }
581