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