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