Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 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 | |
Jeff Sharkey | e0c2995 | 2018-02-20 17:24:55 -0700 | [diff] [blame] | 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.TRANSPORT_CELLULAR; |
| 24 | |
| 25 | import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH; |
| 26 | |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 27 | import android.app.usage.NetworkStatsManager; |
| 28 | import android.app.usage.NetworkStatsManager.UsageCallback; |
| 29 | import android.content.Context; |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 30 | import android.net.ConnectivityManager; |
| 31 | import android.net.ConnectivityManager.NetworkCallback; |
| 32 | import android.net.Network; |
| 33 | import android.net.NetworkCapabilities; |
| 34 | import android.net.NetworkPolicyManager; |
| 35 | import android.net.NetworkRequest; |
| 36 | import android.net.NetworkStats; |
| 37 | import android.net.NetworkTemplate; |
| 38 | import android.net.StringNetworkSpecifier; |
| 39 | import android.os.Handler; |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 40 | import android.telephony.TelephonyManager; |
| 41 | import android.util.DebugUtils; |
| 42 | import android.util.Slog; |
| 43 | |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 44 | import com.android.internal.util.IndentingPrintWriter; |
| 45 | import com.android.server.LocalServices; |
| 46 | import com.android.server.net.NetworkPolicyManagerInternal; |
Jeff Sharkey | e0c2995 | 2018-02-20 17:24:55 -0700 | [diff] [blame] | 47 | import com.android.server.net.NetworkStatsManagerInternal; |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 48 | |
Jeff Sharkey | e0c2995 | 2018-02-20 17:24:55 -0700 | [diff] [blame] | 49 | import java.util.Calendar; |
| 50 | import java.util.concurrent.ConcurrentHashMap; |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 51 | |
| 52 | /** |
| 53 | * Manages multipath data budgets. |
| 54 | * |
| 55 | * Informs the return value of ConnectivityManager#getMultipathPreference() based on: |
| 56 | * - The user's data plan, as returned by getSubscriptionOpportunisticQuota(). |
| 57 | * - The amount of data usage that occurs on mobile networks while they are not the system default |
| 58 | * network (i.e., when the app explicitly selected such networks). |
| 59 | * |
| 60 | * Currently, quota is determined on a daily basis, from midnight to midnight local time. |
| 61 | * |
| 62 | * @hide |
| 63 | */ |
| 64 | public class MultipathPolicyTracker { |
| 65 | private static String TAG = MultipathPolicyTracker.class.getSimpleName(); |
| 66 | |
| 67 | private static final boolean DBG = false; |
| 68 | |
| 69 | private final Context mContext; |
| 70 | private final Handler mHandler; |
| 71 | |
| 72 | private ConnectivityManager mCM; |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 73 | private NetworkPolicyManager mNPM; |
Jeff Sharkey | e0c2995 | 2018-02-20 17:24:55 -0700 | [diff] [blame] | 74 | private NetworkStatsManager mStatsManager; |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 75 | |
| 76 | private NetworkCallback mMobileNetworkCallback; |
| 77 | private NetworkPolicyManager.Listener mPolicyListener; |
| 78 | |
| 79 | // STOPSHIP: replace this with a configurable mechanism. |
| 80 | private static final long DEFAULT_DAILY_MULTIPATH_QUOTA = 2_500_000; |
| 81 | |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 82 | public MultipathPolicyTracker(Context ctx, Handler handler) { |
| 83 | mContext = ctx; |
| 84 | mHandler = handler; |
| 85 | // Because we are initialized by the ConnectivityService constructor, we can't touch any |
| 86 | // connectivity APIs. Service initialization is done in start(). |
| 87 | } |
| 88 | |
| 89 | public void start() { |
Jeff Sharkey | e0c2995 | 2018-02-20 17:24:55 -0700 | [diff] [blame] | 90 | mCM = mContext.getSystemService(ConnectivityManager.class); |
| 91 | mNPM = mContext.getSystemService(NetworkPolicyManager.class); |
| 92 | mStatsManager = mContext.getSystemService(NetworkStatsManager.class); |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 93 | |
| 94 | registerTrackMobileCallback(); |
| 95 | registerNetworkPolicyListener(); |
| 96 | } |
| 97 | |
| 98 | public void shutdown() { |
| 99 | maybeUnregisterTrackMobileCallback(); |
| 100 | unregisterNetworkPolicyListener(); |
| 101 | for (MultipathTracker t : mMultipathTrackers.values()) { |
| 102 | t.shutdown(); |
| 103 | } |
| 104 | mMultipathTrackers.clear(); |
| 105 | } |
| 106 | |
| 107 | // Called on an arbitrary binder thread. |
| 108 | public Integer getMultipathPreference(Network network) { |
Jeff Sharkey | e0c2995 | 2018-02-20 17:24:55 -0700 | [diff] [blame] | 109 | if (network == null) { |
| 110 | return null; |
| 111 | } |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 112 | MultipathTracker t = mMultipathTrackers.get(network); |
| 113 | if (t != null) { |
| 114 | return t.getMultipathPreference(); |
| 115 | } |
| 116 | return null; |
| 117 | } |
| 118 | |
| 119 | // Track information on mobile networks as they come and go. |
| 120 | class MultipathTracker { |
| 121 | final Network network; |
| 122 | final int subId; |
| 123 | final String subscriberId; |
| 124 | |
| 125 | private long mQuota; |
| 126 | /** Current multipath budget. Nonzero iff we have budget and a UsageCallback is armed. */ |
| 127 | private long mMultipathBudget; |
| 128 | private final NetworkTemplate mNetworkTemplate; |
| 129 | private final UsageCallback mUsageCallback; |
| 130 | |
| 131 | public MultipathTracker(Network network, NetworkCapabilities nc) { |
| 132 | this.network = network; |
| 133 | try { |
| 134 | subId = Integer.parseInt( |
| 135 | ((StringNetworkSpecifier) nc.getNetworkSpecifier()).toString()); |
| 136 | } catch (ClassCastException | NullPointerException | NumberFormatException e) { |
| 137 | throw new IllegalStateException(String.format( |
| 138 | "Can't get subId from mobile network %s (%s): %s", |
| 139 | network, nc, e.getMessage())); |
| 140 | } |
| 141 | |
Jeff Sharkey | e0c2995 | 2018-02-20 17:24:55 -0700 | [diff] [blame] | 142 | TelephonyManager tele = mContext.getSystemService(TelephonyManager.class); |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 143 | if (tele == null) { |
| 144 | throw new IllegalStateException(String.format("Missing TelephonyManager")); |
| 145 | } |
| 146 | tele = tele.createForSubscriptionId(subId); |
| 147 | if (tele == null) { |
| 148 | throw new IllegalStateException(String.format( |
| 149 | "Can't get TelephonyManager for subId %d", subId)); |
| 150 | } |
| 151 | |
| 152 | subscriberId = tele.getSubscriberId(); |
| 153 | mNetworkTemplate = new NetworkTemplate( |
Jeff Sharkey | e0c2995 | 2018-02-20 17:24:55 -0700 | [diff] [blame] | 154 | NetworkTemplate.MATCH_MOBILE, subscriberId, new String[] { subscriberId }, |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 155 | null, NetworkStats.METERED_ALL, NetworkStats.ROAMING_ALL, |
| 156 | NetworkStats.DEFAULT_NETWORK_NO); |
| 157 | mUsageCallback = new UsageCallback() { |
| 158 | @Override |
| 159 | public void onThresholdReached(int networkType, String subscriberId) { |
| 160 | if (DBG) Slog.d(TAG, "onThresholdReached for network " + network); |
| 161 | mMultipathBudget = 0; |
| 162 | updateMultipathBudget(); |
| 163 | } |
| 164 | }; |
| 165 | |
| 166 | updateMultipathBudget(); |
| 167 | } |
| 168 | |
| 169 | private long getDailyNonDefaultDataUsage() { |
| 170 | Calendar start = Calendar.getInstance(); |
| 171 | Calendar end = (Calendar) start.clone(); |
| 172 | start.set(Calendar.HOUR_OF_DAY, 0); |
| 173 | start.set(Calendar.MINUTE, 0); |
| 174 | start.set(Calendar.SECOND, 0); |
| 175 | start.set(Calendar.MILLISECOND, 0); |
| 176 | |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 177 | try { |
Jeff Sharkey | e0c2995 | 2018-02-20 17:24:55 -0700 | [diff] [blame] | 178 | final long bytes = LocalServices.getService(NetworkStatsManagerInternal.class) |
| 179 | .getNetworkTotalBytes(mNetworkTemplate, start.getTimeInMillis(), |
| 180 | end.getTimeInMillis()); |
| 181 | if (DBG) Slog.d(TAG, "Non-default data usage: " + bytes); |
| 182 | return bytes; |
| 183 | } catch (RuntimeException e) { |
| 184 | Slog.w(TAG, "Failed to get data usage: " + e); |
| 185 | return -1; |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 186 | } |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 187 | } |
| 188 | |
| 189 | void updateMultipathBudget() { |
Jeff Sharkey | e0c2995 | 2018-02-20 17:24:55 -0700 | [diff] [blame] | 190 | long quota = LocalServices.getService(NetworkPolicyManagerInternal.class) |
| 191 | .getSubscriptionOpportunisticQuota(this.network, QUOTA_TYPE_MULTIPATH); |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 192 | if (DBG) Slog.d(TAG, "Opportunistic quota from data plan: " + quota + " bytes"); |
| 193 | |
| 194 | if (quota == 0) { |
| 195 | // STOPSHIP: replace this with a configurable mechanism. |
| 196 | quota = DEFAULT_DAILY_MULTIPATH_QUOTA; |
| 197 | if (DBG) Slog.d(TAG, "Setting quota: " + quota + " bytes"); |
| 198 | } |
| 199 | |
| 200 | if (haveMultipathBudget() && quota == mQuota) { |
| 201 | // If we already have a usage callback pending , there's no need to re-register it |
| 202 | // if the quota hasn't changed. The callback will simply fire as expected when the |
| 203 | // budget is spent. Also: if we re-register the callback when we're below the |
| 204 | // UsageCallback's minimum value of 2MB, we'll overshoot the budget. |
| 205 | if (DBG) Slog.d(TAG, "Quota still " + quota + ", not updating."); |
| 206 | return; |
| 207 | } |
| 208 | mQuota = quota; |
| 209 | |
Jeff Sharkey | e0c2995 | 2018-02-20 17:24:55 -0700 | [diff] [blame] | 210 | // If we can't get current usage, assume the worst and don't give |
| 211 | // ourselves any budget to work with. |
| 212 | final long usage = getDailyNonDefaultDataUsage(); |
| 213 | final long budget = (usage == -1) ? 0 : Math.max(0, quota - usage); |
Lorenzo Colitti | d260ef2 | 2018-01-24 17:35:07 +0900 | [diff] [blame] | 214 | if (budget > 0) { |
| 215 | if (DBG) Slog.d(TAG, "Setting callback for " + budget + |
| 216 | " bytes on network " + network); |
| 217 | registerUsageCallback(budget); |
| 218 | } else { |
| 219 | maybeUnregisterUsageCallback(); |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | public int getMultipathPreference() { |
| 224 | if (haveMultipathBudget()) { |
| 225 | return MULTIPATH_PREFERENCE_HANDOVER | MULTIPATH_PREFERENCE_RELIABILITY; |
| 226 | } |
| 227 | return 0; |
| 228 | } |
| 229 | |
| 230 | // For debugging only. |
| 231 | public long getQuota() { |
| 232 | return mQuota; |
| 233 | } |
| 234 | |
| 235 | // For debugging only. |
| 236 | public long getMultipathBudget() { |
| 237 | return mMultipathBudget; |
| 238 | } |
| 239 | |
| 240 | private boolean haveMultipathBudget() { |
| 241 | return mMultipathBudget > 0; |
| 242 | } |
| 243 | |
| 244 | private void registerUsageCallback(long budget) { |
| 245 | maybeUnregisterUsageCallback(); |
| 246 | mStatsManager.registerUsageCallback(mNetworkTemplate, TYPE_MOBILE, budget, |
| 247 | mUsageCallback, mHandler); |
| 248 | mMultipathBudget = budget; |
| 249 | } |
| 250 | |
| 251 | private void maybeUnregisterUsageCallback() { |
| 252 | if (haveMultipathBudget()) { |
| 253 | if (DBG) Slog.d(TAG, "Unregistering callback, budget was " + mMultipathBudget); |
| 254 | mStatsManager.unregisterUsageCallback(mUsageCallback); |
| 255 | mMultipathBudget = 0; |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | void shutdown() { |
| 260 | maybeUnregisterUsageCallback(); |
| 261 | } |
| 262 | } |
| 263 | |
| 264 | // Only ever updated on the handler thread. Accessed from other binder threads to retrieve |
| 265 | // the tracker for a specific network. |
| 266 | private final ConcurrentHashMap <Network, MultipathTracker> mMultipathTrackers = |
| 267 | new ConcurrentHashMap<>(); |
| 268 | |
| 269 | // TODO: this races with app code that might respond to onAvailable() by immediately calling |
| 270 | // getMultipathPreference. Fix this by adding to ConnectivityService the ability to directly |
| 271 | // invoke NetworkCallbacks on tightly-coupled classes such as this one which run on its |
| 272 | // handler thread. |
| 273 | private void registerTrackMobileCallback() { |
| 274 | final NetworkRequest request = new NetworkRequest.Builder() |
| 275 | .addCapability(NET_CAPABILITY_INTERNET) |
| 276 | .addTransportType(TRANSPORT_CELLULAR) |
| 277 | .build(); |
| 278 | mMobileNetworkCallback = new ConnectivityManager.NetworkCallback() { |
| 279 | @Override |
| 280 | public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { |
| 281 | MultipathTracker existing = mMultipathTrackers.get(network); |
| 282 | if (existing != null) { |
| 283 | existing.updateMultipathBudget(); |
| 284 | return; |
| 285 | } |
| 286 | |
| 287 | try { |
| 288 | mMultipathTrackers.put(network, new MultipathTracker(network, nc)); |
| 289 | } catch (IllegalStateException e) { |
| 290 | Slog.e(TAG, "Can't track mobile network " + network + ": " + e.getMessage()); |
| 291 | } |
| 292 | if (DBG) Slog.d(TAG, "Tracking mobile network " + network); |
| 293 | } |
| 294 | |
| 295 | @Override |
| 296 | public void onLost(Network network) { |
| 297 | MultipathTracker existing = mMultipathTrackers.get(network); |
| 298 | if (existing != null) { |
| 299 | existing.shutdown(); |
| 300 | mMultipathTrackers.remove(network); |
| 301 | } |
| 302 | if (DBG) Slog.d(TAG, "No longer tracking mobile network " + network); |
| 303 | } |
| 304 | }; |
| 305 | |
| 306 | mCM.registerNetworkCallback(request, mMobileNetworkCallback, mHandler); |
| 307 | } |
| 308 | |
| 309 | private void maybeUnregisterTrackMobileCallback() { |
| 310 | if (mMobileNetworkCallback != null) { |
| 311 | mCM.unregisterNetworkCallback(mMobileNetworkCallback); |
| 312 | } |
| 313 | mMobileNetworkCallback = null; |
| 314 | } |
| 315 | |
| 316 | private void registerNetworkPolicyListener() { |
| 317 | mPolicyListener = new NetworkPolicyManager.Listener() { |
| 318 | @Override |
| 319 | public void onMeteredIfacesChanged(String[] meteredIfaces) { |
| 320 | // Dispatched every time opportunistic quota is recalculated. |
| 321 | mHandler.post(() -> { |
| 322 | for (MultipathTracker t : mMultipathTrackers.values()) { |
| 323 | t.updateMultipathBudget(); |
| 324 | } |
| 325 | }); |
| 326 | } |
| 327 | }; |
| 328 | mNPM.registerListener(mPolicyListener); |
| 329 | } |
| 330 | |
| 331 | private void unregisterNetworkPolicyListener() { |
| 332 | mNPM.unregisterListener(mPolicyListener); |
| 333 | } |
| 334 | |
| 335 | public void dump(IndentingPrintWriter pw) { |
| 336 | // Do not use in production. Access to class data is only safe on the handler thrad. |
| 337 | pw.println("MultipathPolicyTracker:"); |
| 338 | pw.increaseIndent(); |
| 339 | for (MultipathTracker t : mMultipathTrackers.values()) { |
| 340 | pw.println(String.format("Network %s: quota %d, budget %d. Preference: %s", |
| 341 | t.network, t.getQuota(), t.getMultipathBudget(), |
| 342 | DebugUtils.flagsToString(ConnectivityManager.class, "MULTIPATH_PREFERENCE_", |
| 343 | t.getMultipathPreference()))); |
| 344 | } |
| 345 | pw.decreaseIndent(); |
| 346 | } |
| 347 | } |