summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/ConnectivityService.java15
-rw-r--r--services/core/java/com/android/server/MultipathPolicyTracker.java361
-rw-r--r--services/net/java/android/net/util/MultinetworkPolicyTracker.java1
3 files changed, 377 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 145b307728be..3bfa31a80b9d 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -140,6 +140,7 @@ import com.android.server.connectivity.IpConnectivityMetrics;
import com.android.server.connectivity.KeepaliveTracker;
import com.android.server.connectivity.LingerMonitor;
import com.android.server.connectivity.MockableSystemProperties;
+import com.android.server.connectivity.MultipathPolicyTracker;
import com.android.server.connectivity.NetworkAgentInfo;
import com.android.server.connectivity.NetworkDiagnostics;
import com.android.server.connectivity.NetworkMonitor;
@@ -511,6 +512,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
@VisibleForTesting
final MultinetworkPolicyTracker mMultinetworkPolicyTracker;
+ @VisibleForTesting
+ final MultipathPolicyTracker mMultipathPolicyTracker;
+
/**
* Implements support for the legacy "one network per network type" model.
*
@@ -894,6 +898,8 @@ public class ConnectivityService extends IConnectivityManager.Stub
mContext, mHandler, () -> rematchForAvoidBadWifiUpdate());
mMultinetworkPolicyTracker.start();
+ mMultipathPolicyTracker = new MultipathPolicyTracker(mContext, mHandler);
+
mDnsManager = new DnsManager(mContext, mNetd, mSystemProperties);
registerPrivateDnsSettingsCallbacks();
}
@@ -1974,6 +1980,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
pw.println();
dumpAvoidBadWifiSettings(pw);
+ pw.println();
+ mMultipathPolicyTracker.dump(pw);
+
if (argsContain(args, SHORT_ARG) == false) {
pw.println();
synchronized (mValidationLogs) {
@@ -2891,6 +2900,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
return ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED;
}
+ Integer networkPreference = mMultipathPolicyTracker.getMultipathPreference(network);
+ if (networkPreference != null) {
+ return networkPreference;
+ }
+
return mMultinetworkPolicyTracker.getMeteredMultipathPreference();
}
@@ -2984,6 +2998,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
nai.networkMonitor.systemReady = true;
}
+ mMultipathPolicyTracker.start();
break;
}
case EVENT_REVALIDATE_NETWORK: {
diff --git a/services/core/java/com/android/server/MultipathPolicyTracker.java b/services/core/java/com/android/server/MultipathPolicyTracker.java
new file mode 100644
index 000000000000..9e0a230b9250
--- /dev/null
+++ b/services/core/java/com/android/server/MultipathPolicyTracker.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.app.usage.NetworkStatsManager;
+import android.app.usage.NetworkStatsManager.UsageCallback;
+import android.content.Context;
+import android.net.INetworkStatsService;
+import android.net.INetworkPolicyManager;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkPolicyManager;
+import android.net.NetworkRequest;
+import android.net.NetworkStats;
+import android.net.NetworkTemplate;
+import android.net.StringNetworkSpecifier;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.telephony.TelephonyManager;
+import android.util.DebugUtils;
+import android.util.Slog;
+
+import java.util.Calendar;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.LocalServices;
+import com.android.server.net.NetworkPolicyManagerInternal;
+
+import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_HANDOVER;
+import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.provider.Settings.Global.NETWORK_AVOID_BAD_WIFI;
+import static android.provider.Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE;
+import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH;
+
+/**
+ * Manages multipath data budgets.
+ *
+ * Informs the return value of ConnectivityManager#getMultipathPreference() based on:
+ * - The user's data plan, as returned by getSubscriptionOpportunisticQuota().
+ * - The amount of data usage that occurs on mobile networks while they are not the system default
+ * network (i.e., when the app explicitly selected such networks).
+ *
+ * Currently, quota is determined on a daily basis, from midnight to midnight local time.
+ *
+ * @hide
+ */
+public class MultipathPolicyTracker {
+ private static String TAG = MultipathPolicyTracker.class.getSimpleName();
+
+ private static final boolean DBG = false;
+
+ private final Context mContext;
+ private final Handler mHandler;
+
+ private ConnectivityManager mCM;
+ private NetworkStatsManager mStatsManager;
+ private NetworkPolicyManager mNPM;
+ private TelephonyManager mTelephonyManager;
+ private INetworkStatsService mStatsService;
+
+ private NetworkCallback mMobileNetworkCallback;
+ private NetworkPolicyManager.Listener mPolicyListener;
+
+ // STOPSHIP: replace this with a configurable mechanism.
+ private static final long DEFAULT_DAILY_MULTIPATH_QUOTA = 2_500_000;
+
+ private volatile int mMeteredMultipathPreference;
+
+ public MultipathPolicyTracker(Context ctx, Handler handler) {
+ mContext = ctx;
+ mHandler = handler;
+ // Because we are initialized by the ConnectivityService constructor, we can't touch any
+ // connectivity APIs. Service initialization is done in start().
+ }
+
+ public void start() {
+ mCM = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mNPM = (NetworkPolicyManager) mContext.getSystemService(Context.NETWORK_POLICY_SERVICE);
+ mStatsManager = (NetworkStatsManager) mContext.getSystemService(
+ Context.NETWORK_STATS_SERVICE);
+ mStatsService = INetworkStatsService.Stub.asInterface(
+ ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
+
+ registerTrackMobileCallback();
+ registerNetworkPolicyListener();
+ }
+
+ public void shutdown() {
+ maybeUnregisterTrackMobileCallback();
+ unregisterNetworkPolicyListener();
+ for (MultipathTracker t : mMultipathTrackers.values()) {
+ t.shutdown();
+ }
+ mMultipathTrackers.clear();
+ }
+
+ // Called on an arbitrary binder thread.
+ public Integer getMultipathPreference(Network network) {
+ MultipathTracker t = mMultipathTrackers.get(network);
+ if (t != null) {
+ return t.getMultipathPreference();
+ }
+ return null;
+ }
+
+ // Track information on mobile networks as they come and go.
+ class MultipathTracker {
+ final Network network;
+ final int subId;
+ final String subscriberId;
+
+ private long mQuota;
+ /** Current multipath budget. Nonzero iff we have budget and a UsageCallback is armed. */
+ private long mMultipathBudget;
+ private final NetworkTemplate mNetworkTemplate;
+ private final UsageCallback mUsageCallback;
+
+ public MultipathTracker(Network network, NetworkCapabilities nc) {
+ this.network = network;
+ try {
+ subId = Integer.parseInt(
+ ((StringNetworkSpecifier) nc.getNetworkSpecifier()).toString());
+ } catch (ClassCastException | NullPointerException | NumberFormatException e) {
+ throw new IllegalStateException(String.format(
+ "Can't get subId from mobile network %s (%s): %s",
+ network, nc, e.getMessage()));
+ }
+
+ TelephonyManager tele = (TelephonyManager) mContext.getSystemService(
+ Context.TELEPHONY_SERVICE);
+ if (tele == null) {
+ throw new IllegalStateException(String.format("Missing TelephonyManager"));
+ }
+ tele = tele.createForSubscriptionId(subId);
+ if (tele == null) {
+ throw new IllegalStateException(String.format(
+ "Can't get TelephonyManager for subId %d", subId));
+ }
+
+ subscriberId = tele.getSubscriberId();
+ mNetworkTemplate = new NetworkTemplate(
+ NetworkTemplate.MATCH_MOBILE_ALL, subscriberId, new String[] { subscriberId },
+ null, NetworkStats.METERED_ALL, NetworkStats.ROAMING_ALL,
+ NetworkStats.DEFAULT_NETWORK_NO);
+ mUsageCallback = new UsageCallback() {
+ @Override
+ public void onThresholdReached(int networkType, String subscriberId) {
+ if (DBG) Slog.d(TAG, "onThresholdReached for network " + network);
+ mMultipathBudget = 0;
+ updateMultipathBudget();
+ }
+ };
+
+ updateMultipathBudget();
+ }
+
+ private long getDailyNonDefaultDataUsage() {
+ Calendar start = Calendar.getInstance();
+ Calendar end = (Calendar) start.clone();
+ start.set(Calendar.HOUR_OF_DAY, 0);
+ start.set(Calendar.MINUTE, 0);
+ start.set(Calendar.SECOND, 0);
+ start.set(Calendar.MILLISECOND, 0);
+
+ long bytes;
+ try {
+ // TODO: Consider using NetworkStatsManager.getSummaryForDevice instead.
+ bytes = mStatsService.getNetworkTotalBytes(mNetworkTemplate,
+ start.getTimeInMillis(), end.getTimeInMillis());
+ if (DBG) Slog.w(TAG, "Non-default data usage: " + bytes);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Can't fetch daily data usage: " + e);
+ bytes = -1;
+ } catch (IllegalStateException e) {
+ // Bandwidth control disabled?
+ bytes = -1;
+ }
+ return bytes;
+ }
+
+ void updateMultipathBudget() {
+ NetworkPolicyManagerInternal npms = LocalServices.getService(
+ NetworkPolicyManagerInternal.class);
+ long quota = npms.getSubscriptionOpportunisticQuota(this.network, QUOTA_TYPE_MULTIPATH);
+ if (DBG) Slog.d(TAG, "Opportunistic quota from data plan: " + quota + " bytes");
+
+ if (quota == 0) {
+ // STOPSHIP: replace this with a configurable mechanism.
+ quota = DEFAULT_DAILY_MULTIPATH_QUOTA;
+ if (DBG) Slog.d(TAG, "Setting quota: " + quota + " bytes");
+ }
+
+ if (haveMultipathBudget() && quota == mQuota) {
+ // If we already have a usage callback pending , there's no need to re-register it
+ // if the quota hasn't changed. The callback will simply fire as expected when the
+ // budget is spent. Also: if we re-register the callback when we're below the
+ // UsageCallback's minimum value of 2MB, we'll overshoot the budget.
+ if (DBG) Slog.d(TAG, "Quota still " + quota + ", not updating.");
+ return;
+ }
+ mQuota = quota;
+
+ long usage = getDailyNonDefaultDataUsage();
+ long budget = Math.max(0, quota - usage);
+ if (budget > 0) {
+ if (DBG) Slog.d(TAG, "Setting callback for " + budget +
+ " bytes on network " + network);
+ registerUsageCallback(budget);
+ } else {
+ maybeUnregisterUsageCallback();
+ }
+ }
+
+ public int getMultipathPreference() {
+ if (haveMultipathBudget()) {
+ return MULTIPATH_PREFERENCE_HANDOVER | MULTIPATH_PREFERENCE_RELIABILITY;
+ }
+ return 0;
+ }
+
+ // For debugging only.
+ public long getQuota() {
+ return mQuota;
+ }
+
+ // For debugging only.
+ public long getMultipathBudget() {
+ return mMultipathBudget;
+ }
+
+ private boolean haveMultipathBudget() {
+ return mMultipathBudget > 0;
+ }
+
+ private void registerUsageCallback(long budget) {
+ maybeUnregisterUsageCallback();
+ mStatsManager.registerUsageCallback(mNetworkTemplate, TYPE_MOBILE, budget,
+ mUsageCallback, mHandler);
+ mMultipathBudget = budget;
+ }
+
+ private void maybeUnregisterUsageCallback() {
+ if (haveMultipathBudget()) {
+ if (DBG) Slog.d(TAG, "Unregistering callback, budget was " + mMultipathBudget);
+ mStatsManager.unregisterUsageCallback(mUsageCallback);
+ mMultipathBudget = 0;
+ }
+ }
+
+ void shutdown() {
+ maybeUnregisterUsageCallback();
+ }
+ }
+
+ // Only ever updated on the handler thread. Accessed from other binder threads to retrieve
+ // the tracker for a specific network.
+ private final ConcurrentHashMap <Network, MultipathTracker> mMultipathTrackers =
+ new ConcurrentHashMap<>();
+
+ // TODO: this races with app code that might respond to onAvailable() by immediately calling
+ // getMultipathPreference. Fix this by adding to ConnectivityService the ability to directly
+ // invoke NetworkCallbacks on tightly-coupled classes such as this one which run on its
+ // handler thread.
+ private void registerTrackMobileCallback() {
+ final NetworkRequest request = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addTransportType(TRANSPORT_CELLULAR)
+ .build();
+ mMobileNetworkCallback = new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
+ MultipathTracker existing = mMultipathTrackers.get(network);
+ if (existing != null) {
+ existing.updateMultipathBudget();
+ return;
+ }
+
+ try {
+ mMultipathTrackers.put(network, new MultipathTracker(network, nc));
+ } catch (IllegalStateException e) {
+ Slog.e(TAG, "Can't track mobile network " + network + ": " + e.getMessage());
+ }
+ if (DBG) Slog.d(TAG, "Tracking mobile network " + network);
+ }
+
+ @Override
+ public void onLost(Network network) {
+ MultipathTracker existing = mMultipathTrackers.get(network);
+ if (existing != null) {
+ existing.shutdown();
+ mMultipathTrackers.remove(network);
+ }
+ if (DBG) Slog.d(TAG, "No longer tracking mobile network " + network);
+ }
+ };
+
+ mCM.registerNetworkCallback(request, mMobileNetworkCallback, mHandler);
+ }
+
+ private void maybeUnregisterTrackMobileCallback() {
+ if (mMobileNetworkCallback != null) {
+ mCM.unregisterNetworkCallback(mMobileNetworkCallback);
+ }
+ mMobileNetworkCallback = null;
+ }
+
+ private void registerNetworkPolicyListener() {
+ mPolicyListener = new NetworkPolicyManager.Listener() {
+ @Override
+ public void onMeteredIfacesChanged(String[] meteredIfaces) {
+ // Dispatched every time opportunistic quota is recalculated.
+ mHandler.post(() -> {
+ for (MultipathTracker t : mMultipathTrackers.values()) {
+ t.updateMultipathBudget();
+ }
+ });
+ }
+ };
+ mNPM.registerListener(mPolicyListener);
+ }
+
+ private void unregisterNetworkPolicyListener() {
+ mNPM.unregisterListener(mPolicyListener);
+ }
+
+ public void dump(IndentingPrintWriter pw) {
+ // Do not use in production. Access to class data is only safe on the handler thrad.
+ pw.println("MultipathPolicyTracker:");
+ pw.increaseIndent();
+ for (MultipathTracker t : mMultipathTrackers.values()) {
+ pw.println(String.format("Network %s: quota %d, budget %d. Preference: %s",
+ t.network, t.getQuota(), t.getMultipathBudget(),
+ DebugUtils.flagsToString(ConnectivityManager.class, "MULTIPATH_PREFERENCE_",
+ t.getMultipathPreference())));
+ }
+ pw.decreaseIndent();
+ }
+}
diff --git a/services/net/java/android/net/util/MultinetworkPolicyTracker.java b/services/net/java/android/net/util/MultinetworkPolicyTracker.java
index 424e40d2096f..30c5cd98b719 100644
--- a/services/net/java/android/net/util/MultinetworkPolicyTracker.java
+++ b/services/net/java/android/net/util/MultinetworkPolicyTracker.java
@@ -122,6 +122,7 @@ public class MultinetworkPolicyTracker {
return mAvoidBadWifi;
}
+ // TODO: move this to MultipathPolicyTracker.
public int getMeteredMultipathPreference() {
return mMeteredMultipathPreference;
}