blob: 6fa999cb039aeedf002c095a3eda74ed92542914 [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 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.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkPolicy.LIMIT_DISABLED;
import static android.net.NetworkPolicy.WARNING_DISABLED;
import static android.provider.Settings.Global.NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES;
import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH;
import static com.android.server.net.NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN;
import android.app.usage.NetworkStatsManager;
import android.app.usage.NetworkStatsManager.UsageCallback;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.ContentObserver;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkIdentity;
import android.net.NetworkPolicy;
import android.net.NetworkPolicyManager;
import android.net.NetworkRequest;
import android.net.NetworkStats;
import android.net.NetworkTemplate;
import android.net.StringNetworkSpecifier;
import android.os.BestClock;
import android.os.Handler;
import android.os.SystemClock;
import android.net.Uri;
import android.os.UserHandle;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.util.DataUnit;
import android.util.DebugUtils;
import android.util.Pair;
import android.util.Range;
import android.util.Slog;
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 com.android.server.net.NetworkStatsManagerInternal;
import java.time.Clock;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* 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 final Clock mClock;
private final Dependencies mDeps;
private final ContentResolver mResolver;
private final ConfigChangeReceiver mConfigChangeReceiver;
@VisibleForTesting
final ContentObserver mSettingsObserver;
private ConnectivityManager mCM;
private NetworkPolicyManager mNPM;
private NetworkStatsManager mStatsManager;
private NetworkCallback mMobileNetworkCallback;
private NetworkPolicyManager.Listener mPolicyListener;
/**
* Divider to calculate opportunistic quota from user-set data limit or warning: 5% of user-set
* limit.
*/
private static final int OPQUOTA_USER_SETTING_DIVIDER = 20;
public static class Dependencies {
public Clock getClock() {
return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
Clock.systemUTC());
}
}
public MultipathPolicyTracker(Context ctx, Handler handler) {
this(ctx, handler, new Dependencies());
}
public MultipathPolicyTracker(Context ctx, Handler handler, Dependencies deps) {
mContext = ctx;
mHandler = handler;
mClock = deps.getClock();
mDeps = deps;
mResolver = mContext.getContentResolver();
mSettingsObserver = new SettingsObserver(mHandler);
mConfigChangeReceiver = new ConfigChangeReceiver();
// 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 = mContext.getSystemService(ConnectivityManager.class);
mNPM = mContext.getSystemService(NetworkPolicyManager.class);
mStatsManager = mContext.getSystemService(NetworkStatsManager.class);
registerTrackMobileCallback();
registerNetworkPolicyListener();
final Uri defaultSettingUri =
Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES);
mResolver.registerContentObserver(defaultSettingUri, false, mSettingsObserver);
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
mContext.registerReceiverAsUser(
mConfigChangeReceiver, UserHandle.ALL, intentFilter, null, mHandler);
}
public void shutdown() {
maybeUnregisterTrackMobileCallback();
unregisterNetworkPolicyListener();
for (MultipathTracker t : mMultipathTrackers.values()) {
t.shutdown();
}
mMultipathTrackers.clear();
mResolver.unregisterContentObserver(mSettingsObserver);
mContext.unregisterReceiver(mConfigChangeReceiver);
}
// Called on an arbitrary binder thread.
public Integer getMultipathPreference(Network network) {
if (network == null) {
return null;
}
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;
private NetworkCapabilities mNetworkCapabilities;
public MultipathTracker(Network network, NetworkCapabilities nc) {
this.network = network;
this.mNetworkCapabilities = new NetworkCapabilities(nc);
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 = mContext.getSystemService(TelephonyManager.class);
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, 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();
}
public void setNetworkCapabilities(NetworkCapabilities nc) {
mNetworkCapabilities = new NetworkCapabilities(nc);
}
// TODO: calculate with proper timezone information
private long getDailyNonDefaultDataUsage() {
final ZonedDateTime end =
ZonedDateTime.ofInstant(mClock.instant(), ZoneId.systemDefault());
final ZonedDateTime start = end.truncatedTo(ChronoUnit.DAYS);
final long bytes = getNetworkTotalBytes(
start.toInstant().toEpochMilli(),
end.toInstant().toEpochMilli());
if (DBG) Slog.d(TAG, "Non-default data usage: " + bytes);
return bytes;
}
private long getNetworkTotalBytes(long start, long end) {
try {
return LocalServices.getService(NetworkStatsManagerInternal.class)
.getNetworkTotalBytes(mNetworkTemplate, start, end);
} catch (RuntimeException e) {
Slog.w(TAG, "Failed to get data usage: " + e);
return -1;
}
}
private NetworkIdentity getTemplateMatchingNetworkIdentity(NetworkCapabilities nc) {
return new NetworkIdentity(
ConnectivityManager.TYPE_MOBILE,
0 /* subType, unused for template matching */,
subscriberId,
null /* networkId, unused for matching mobile networks */,
!nc.hasCapability(NET_CAPABILITY_NOT_ROAMING),
!nc.hasCapability(NET_CAPABILITY_NOT_METERED),
false /* defaultNetwork, templates should have DEFAULT_NETWORK_ALL */);
}
private long getRemainingDailyBudget(long limitBytes,
Range<ZonedDateTime> cycle) {
final long start = cycle.getLower().toInstant().toEpochMilli();
final long end = cycle.getUpper().toInstant().toEpochMilli();
final long totalBytes = getNetworkTotalBytes(start, end);
final long remainingBytes = totalBytes == -1 ? 0 : Math.max(0, limitBytes - totalBytes);
// 1 + ((end - now - 1) / millisInDay with integers is equivalent to:
// ceil((double)(end - now) / millisInDay)
final long remainingDays =
1 + ((end - mClock.millis() - 1) / TimeUnit.DAYS.toMillis(1));
return remainingBytes / Math.max(1, remainingDays);
}
private long getUserPolicyOpportunisticQuotaBytes() {
// Keep the most restrictive applicable policy
long minQuota = Long.MAX_VALUE;
final NetworkIdentity identity = getTemplateMatchingNetworkIdentity(
mNetworkCapabilities);
final NetworkPolicy[] policies = mNPM.getNetworkPolicies();
for (NetworkPolicy policy : policies) {
if (policy.hasCycle() && policy.template.matches(identity)) {
final long cycleStart = policy.cycleIterator().next().getLower()
.toInstant().toEpochMilli();
// Prefer user-defined warning, otherwise use hard limit
final long activeWarning = getActiveWarning(policy, cycleStart);
final long policyBytes = (activeWarning == WARNING_DISABLED)
? getActiveLimit(policy, cycleStart)
: activeWarning;
if (policyBytes != LIMIT_DISABLED && policyBytes != WARNING_DISABLED) {
final long policyBudget = getRemainingDailyBudget(policyBytes,
policy.cycleIterator().next());
minQuota = Math.min(minQuota, policyBudget);
}
}
}
if (minQuota == Long.MAX_VALUE) {
return OPPORTUNISTIC_QUOTA_UNKNOWN;
}
return minQuota / OPQUOTA_USER_SETTING_DIVIDER;
}
void updateMultipathBudget() {
long quota = LocalServices.getService(NetworkPolicyManagerInternal.class)
.getSubscriptionOpportunisticQuota(this.network, QUOTA_TYPE_MULTIPATH);
if (DBG) Slog.d(TAG, "Opportunistic quota from data plan: " + quota + " bytes");
// Fallback to user settings-based quota if not available from phone plan
if (quota == OPPORTUNISTIC_QUOTA_UNKNOWN) {
quota = getUserPolicyOpportunisticQuotaBytes();
if (DBG) Slog.d(TAG, "Opportunistic quota from user policy: " + quota + " bytes");
}
if (quota == OPPORTUNISTIC_QUOTA_UNKNOWN) {
quota = getDefaultDailyMultipathQuotaBytes();
if (DBG) Slog.d(TAG, "Setting quota: " + quota + " bytes");
}
// TODO: re-register if day changed: budget may have run out but should be refreshed.
if (haveMultipathBudget() && quota == mQuota) {
// If there is already 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.
if (DBG) Slog.d(TAG, "Quota still " + quota + ", not updating.");
return;
}
mQuota = quota;
// If we can't get current usage, assume the worst and don't give
// ourselves any budget to work with.
final long usage = getDailyNonDefaultDataUsage();
final long budget = (usage == -1) ? 0 : Math.max(0, quota - usage);
// Only consider budgets greater than MIN_THRESHOLD_BYTES, otherwise the callback will
// fire late, after data usage went over budget. Also budget should be 0 if remaining
// data is close to 0.
// This is necessary because the usage callback does not accept smaller thresholds.
// Because it snaps everything to MIN_THRESHOLD_BYTES, the lesser of the two evils is
// to snap to 0 here.
// This will only be called if the total quota for the day changed, not if usage changed
// since last time, so even if this is called very often the budget will not snap to 0
// as soon as there are less than 2MB left for today.
if (budget > NetworkStatsManager.MIN_THRESHOLD_BYTES) {
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();
}
}
private static long getActiveWarning(NetworkPolicy policy, long cycleStart) {
return policy.lastWarningSnooze < cycleStart
? policy.warningBytes
: WARNING_DISABLED;
}
private static long getActiveLimit(NetworkPolicy policy, long cycleStart) {
return policy.lastLimitSnooze < cycleStart
? policy.limitBytes
: LIMIT_DISABLED;
}
// 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<>();
private long getDefaultDailyMultipathQuotaBytes() {
final String setting = Settings.Global.getString(mContext.getContentResolver(),
NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES);
if (setting != null) {
try {
return Long.parseLong(setting);
} catch(NumberFormatException e) {
// fall through
}
}
return mContext.getResources().getInteger(
R.integer.config_networkDefaultDailyMultipathQuotaBytes);
}
// 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.setNetworkCapabilities(nc);
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);
}
/**
* Update multipath budgets for all trackers. To be called on the mHandler thread.
*/
private void updateAllMultipathBudgets() {
for (MultipathTracker t : mMultipathTrackers.values()) {
t.updateMultipathBudget();
}
}
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(() -> updateAllMultipathBudgets());
}
};
mNPM.registerListener(mPolicyListener);
}
private void unregisterNetworkPolicyListener() {
mNPM.unregisterListener(mPolicyListener);
}
private final class SettingsObserver extends ContentObserver {
public SettingsObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
Slog.wtf(TAG, "Should never be reached.");
}
@Override
public void onChange(boolean selfChange, Uri uri) {
if (!Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES)
.equals(uri)) {
Slog.wtf(TAG, "Unexpected settings observation: " + uri);
}
if (DBG) Slog.d(TAG, "Settings change: updating budgets.");
updateAllMultipathBudgets();
}
}
private final class ConfigChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (DBG) Slog.d(TAG, "Configuration change: updating budgets.");
updateAllMultipathBudgets();
}
}
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();
}
}