Add settings to configure default multipath quota.

Bug: 72631572
Test: Tests in go/ag/3828171 pass
Change-Id: I795debd0328ea7cad32c968cb4b407928e054528
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 70f343e..b4016ce 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9301,6 +9301,15 @@
                "network_metered_multipath_preference";
 
         /**
+         * Default daily multipath budget used by ConnectivityManager.getMultipathPreference()
+         * on metered networks. This default quota is only used if quota could not be determined
+         * from data plan or data limit/warning set by the user.
+         * @hide
+         */
+        public static final String NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES =
+                "network_default_daily_multipath_quota_bytes";
+
+        /**
          * Network watchlist last report time.
          * @hide
          */
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index a52a089..776f9a7 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -328,6 +328,13 @@
          This is the default value of that setting. -->
     <integer translatable="false" name="config_networkMeteredMultipathPreference">0</integer>
 
+    <!-- Default daily multipath budget used by ConnectivityManager.getMultipathPreference()
+         on metered networks. This default quota only used if quota could not be determined from
+         data plan or data limit/warning set by the user. The value that is actually used is
+         controlled by Settings.Global.NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES. This is the
+         default value of that setting. -->
+    <integer translatable="false" name="config_networkDefaultDailyMultipathQuotaBytes">2500000</integer>
+
     <!-- List of regexpressions describing the interface (if any) that represent tetherable
          USB interfaces.  If the device doesn't want to support tethering over USB this should
          be empty.  An example would be "usb.*" -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 03acd5c..ee7f8fb 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1914,6 +1914,7 @@
   <java-symbol type="integer" name="config_networkWakeupPacketMask" />
   <java-symbol type="bool" name="config_apfDrop802_3Frames" />
   <java-symbol type="array" name="config_apfEthTypeBlackList" />
+  <java-symbol type="integer" name="config_networkDefaultDailyMultipathQuotaBytes" />
   <java-symbol type="integer" name="config_networkMeteredMultipathPreference" />
   <java-symbol type="integer" name="config_notificationsBatteryFullARGB" />
   <java-symbol type="integer" name="config_notificationsBatteryLedOff" />
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 2a3fcad..7908ea9 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -300,6 +300,7 @@
                     Settings.Global.NETSTATS_UID_TAG_PERSIST_BYTES,
                     Settings.Global.NETSTATS_UID_TAG_ROTATE_AGE,
                     Settings.Global.NETWORK_AVOID_BAD_WIFI,
+                    Settings.Global.NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES,
                     Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE,
                     Settings.Global.NETWORK_WATCHLIST_LAST_REPORT_TIME,
                     Settings.Global.NETWORK_PREFERENCE,
diff --git a/services/core/java/com/android/server/connectivity/MultipathPolicyTracker.java b/services/core/java/com/android/server/connectivity/MultipathPolicyTracker.java
index 53a9544..3868ea6 100644
--- a/services/core/java/com/android/server/connectivity/MultipathPolicyTracker.java
+++ b/services/core/java/com/android/server/connectivity/MultipathPolicyTracker.java
@@ -24,13 +24,19 @@
 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.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;
@@ -45,11 +51,15 @@
 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.DebugUtils;
 import android.util.Pair;
 import android.util.Slog;
 
+import com.android.internal.R;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.LocalServices;
 import com.android.server.net.NetworkPolicyManagerInternal;
@@ -60,7 +70,6 @@
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
-import java.util.Calendar;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 
@@ -85,6 +94,9 @@
     private final Handler mHandler;
     private final Clock mClock;
     private final Dependencies mDeps;
+    private final ContentResolver mResolver;
+    private final SettingsObserver mSettingsObserver;
+    private final ConfigChangeReceiver mConfigChangeReceiver;
 
     private ConnectivityManager mCM;
     private NetworkPolicyManager mNPM;
@@ -93,8 +105,6 @@
     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;
 
     /**
      * Divider to calculate opportunistic quota from user-set data limit or warning: 5% of user-set
@@ -118,6 +128,9 @@
         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().
     }
@@ -129,6 +142,14 @@
 
         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() {
@@ -138,6 +159,8 @@
             t.shutdown();
         }
         mMultipathTrackers.clear();
+        mResolver.unregisterContentObserver(mSettingsObserver);
+        mContext.unregisterReceiver(mConfigChangeReceiver);
     }
 
     // Called on an arbitrary binder thread.
@@ -292,11 +315,11 @@
             // 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) {
-                // STOPSHIP: replace this with a configurable mechanism.
-                quota = DEFAULT_DAILY_MULTIPATH_QUOTA;
+                quota = getDefaultDailyMultipathQuotaBytes();
                 if (DBG) Slog.d(TAG, "Setting quota: " + quota + " bytes");
             }
 
@@ -374,6 +397,21 @@
     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
@@ -415,6 +453,15 @@
         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);
@@ -427,11 +474,7 @@
             @Override
             public void onMeteredIfacesChanged(String[] meteredIfaces) {
                 // Dispatched every time opportunistic quota is recalculated.
-                mHandler.post(() -> {
-                    for (MultipathTracker t : mMultipathTrackers.values()) {
-                        t.updateMultipathBudget();
-                    }
-                });
+                mHandler.post(() -> updateAllMultipathBudgets());
             }
         };
         mNPM.registerListener(mPolicyListener);
@@ -441,6 +484,35 @@
         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:");