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