Java程序  |  557行  |  22.67 KB

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