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