| /* |
| * 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(); |
| } |
| } |