blob: 9e0a230b9250fcc3b6307e1486c36789af9fa4ce [file] [log] [blame]
/*
* 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();
}
}