Home | History | Annotate | Download | only in connectivity
      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