[BR14] Tracker Data Saver status in ConnectivityService

BYPASS_INCLUSIVE_LANGUAGE_REASON=Using public API
Test: atest ConnectivityCoverageTests:android.net.connectivity.com.android.server.CSBpfNetMapsTest
      (on U and V devices respectively)
Bug: 297836825
Fix: 314858283

Change-Id: Ibeac8cd3a33468c6539234255328affc6b3f8ffe
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index a934ddb..fa27d0e 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -16,8 +16,6 @@
 package android.net;
 
 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
-import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
-import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
 import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
 import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST;
 import static android.net.NetworkRequest.Type.LISTEN;
@@ -27,8 +25,6 @@
 import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT;
 import static android.net.QosCallback.QosCallbackRegistrationException;
 
-import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
-
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
@@ -41,16 +37,12 @@
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
-import android.annotation.TargetApi;
-import android.app.Application;
 import android.app.PendingIntent;
 import android.app.admin.DevicePolicyManager;
 import android.compat.annotation.UnsupportedAppUsage;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.net.ConnectivityDiagnosticsManager.DataStallReport.DetectionMethod;
 import android.net.IpSecManager.UdpEncapsulationSocket;
 import android.net.SocketKeepalive.Callback;
@@ -82,8 +74,6 @@
 import android.util.SparseIntArray;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.modules.utils.build.SdkLevel;
 
 import libcore.net.event.NetworkEventDispatcher;
 
@@ -105,7 +95,6 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Class that answers queries about the state of network connectivity. It also
@@ -6262,90 +6251,6 @@
     }
 
     /**
-     * Helper class to track data saver status.
-     *
-     * The class will fetch current data saver status from {@link NetworkPolicyManager} when
-     * initialized, and listening for status changed intent to cache the latest status.
-     *
-     * @hide
-     */
-    @TargetApi(Build.VERSION_CODES.TIRAMISU) // RECEIVER_NOT_EXPORTED requires T.
-    @VisibleForTesting(visibility = PRIVATE)
-    public static class DataSaverStatusTracker extends BroadcastReceiver {
-        private static final Object sDataSaverStatusTrackerLock = new Object();
-
-        private static volatile DataSaverStatusTracker sInstance;
-
-        /**
-         * Gets a static instance of the class.
-         *
-         * @param context A {@link Context} for initialization. Note that since the data saver
-         *                status is global on a device, passing any context is equivalent.
-         * @return The static instance of a {@link DataSaverStatusTracker}.
-         */
-        public static DataSaverStatusTracker getInstance(@NonNull Context context) {
-            if (sInstance == null) {
-                synchronized (sDataSaverStatusTrackerLock) {
-                    if (sInstance == null) {
-                        sInstance = new DataSaverStatusTracker(context);
-                    }
-                }
-            }
-            return sInstance;
-        }
-
-        private final NetworkPolicyManager mNpm;
-        // The value updates on the caller's binder thread or UI thread.
-        private final AtomicBoolean mIsDataSaverEnabled;
-
-        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-        public DataSaverStatusTracker(final Context context) {
-            // To avoid leaks, take the application context.
-            final Context appContext;
-            if (context instanceof Application) {
-                appContext = context;
-            } else {
-                appContext = context.getApplicationContext();
-            }
-
-            if ((appContext.getApplicationInfo().flags & FLAG_PERSISTENT) == 0
-                    && (appContext.getApplicationInfo().flags & FLAG_SYSTEM) == 0) {
-                throw new IllegalStateException("Unexpected caller: "
-                        + appContext.getApplicationInfo().packageName);
-            }
-
-            mNpm = appContext.getSystemService(NetworkPolicyManager.class);
-            final IntentFilter filter = new IntentFilter(
-                    ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED);
-            // The receiver should not receive broadcasts from other Apps.
-            appContext.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED);
-            mIsDataSaverEnabled = new AtomicBoolean();
-            updateDataSaverEnabled();
-        }
-
-        // Runs on caller's UI thread.
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (intent.getAction().equals(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED)) {
-                updateDataSaverEnabled();
-            } else {
-                throw new IllegalStateException("Unexpected intent " + intent);
-            }
-        }
-
-        public boolean getDataSaverEnabled() {
-            return mIsDataSaverEnabled.get();
-        }
-
-        private void updateDataSaverEnabled() {
-            // Uid doesn't really matter, but use a fixed UID to make things clearer.
-            final int dataSaverForCallerUid = mNpm.getRestrictBackgroundStatus(Process.SYSTEM_UID);
-            mIsDataSaverEnabled.set(dataSaverForCallerUid
-                    != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED);
-        }
-    }
-
-    /**
      * Return whether the network is blocked for the given uid and metered condition.
      *
      * Similar to {@link NetworkPolicyManager#isUidNetworkingBlocked}, but directly reads the BPF
@@ -6370,17 +6275,12 @@
     @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
     public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
         final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
-
-        final boolean isDataSaverEnabled;
-        if (SdkLevel.isAtLeastV()) {
-            isDataSaverEnabled = reader.getDataSaverEnabled();
-        } else {
-            final DataSaverStatusTracker dataSaverStatusTracker =
-                    DataSaverStatusTracker.getInstance(mContext);
-            isDataSaverEnabled = dataSaverStatusTracker.getDataSaverEnabled();
-        }
-
-        return reader.isUidNetworkingBlocked(uid, isNetworkMetered, isDataSaverEnabled);
+        // Note that before V, the data saver status in bpf is written by ConnectivityService
+        // when receiving {@link #ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
+        // the status is not synchronized.
+        // On V+, the data saver status is set by platform code when enabling/disabling
+        // data saver, which is synchronized.
+        return reader.isUidNetworkingBlocked(uid, isNetworkMetered, reader.getDataSaverEnabled());
     }
 
     /** @hide */
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 5f67246..cc71153 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -32,6 +32,7 @@
 import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS;
 import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS;
 import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE;
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
 import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
 import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
@@ -1770,6 +1771,19 @@
         mUserAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter,
                 null /* broadcastPermission */, mHandler);
 
+        // This is needed for pre-V devices to propagate the data saver status
+        // to the BPF map. This isn't supported before Android T because BPF maps are
+        // unsupported, and it's also unnecessary on Android V and later versions,
+        // as the platform code handles data saver bit updates. Additionally, checking
+        // the initial data saver status here is superfluous because the intent won't
+        // be sent until the system is ready.
+        if (mDeps.isAtLeastT() && !mDeps.isAtLeastV()) {
+            final IntentFilter dataSaverIntentFilter =
+                    new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED);
+            mUserAllContext.registerReceiver(mDataSaverReceiver, dataSaverIntentFilter,
+                    null /* broadcastPermission */, mHandler);
+        }
+
         // TrackMultiNetworkActivities feature should be enabled by trunk stable flag.
         // But reading the trunk stable flags from mainline modules is not supported yet.
         // So enabling this feature on V+ release.
@@ -7051,6 +7065,32 @@
         }
     };
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    private final BroadcastReceiver mDataSaverReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (mDeps.isAtLeastV()) {
+                throw new IllegalStateException(
+                        "data saver status should be updated from platform");
+            }
+            ensureRunningOnConnectivityServiceThread();
+            switch (intent.getAction()) {
+                case ACTION_RESTRICT_BACKGROUND_CHANGED:
+                    // If the uid is present in the deny list, the API will consistently
+                    // return ENABLED. To retrieve the global switch status, the system
+                    // uid is chosen because it will never be included in the deny list.
+                    final int dataSaverForSystemUid =
+                            mPolicyManager.getRestrictBackgroundStatus(Process.SYSTEM_UID);
+                    final boolean isDataSaverEnabled = (dataSaverForSystemUid
+                            != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED);
+                    mBpfNetMaps.setDataSaverEnabled(isDataSaverEnabled);
+                    break;
+                default:
+                    Log.wtf(TAG, "received unexpected intent: " + intent.getAction());
+            }
+        }
+    };
+
     private final HashMap<Messenger, NetworkProviderInfo> mNetworkProviderInfos = new HashMap<>();
     private final HashMap<NetworkRequest, NetworkRequestInfo> mNetworkRequests = new HashMap<>();
 
@@ -12985,7 +13025,7 @@
         }
     }
 
-    @TargetApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     @Override
     public void setDataSaverEnabled(final boolean enable) {
         enforceNetworkStackOrSettingsPermission();
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index 0082af2..b71a46f 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -16,13 +16,6 @@
 
 package android.net;
 
-import static android.content.Context.RECEIVER_NOT_EXPORTED;
-import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
-import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
 import static android.net.ConnectivityManager.TYPE_NONE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
@@ -46,7 +39,6 @@
 
 import static com.android.testutils.MiscAsserts.assertThrows;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -70,10 +62,7 @@
 
 import android.app.PendingIntent;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
-import android.net.ConnectivityManager.DataSaverStatusTracker;
 import android.net.ConnectivityManager.NetworkCallback;
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
@@ -527,44 +516,6 @@
                     + " attempts", ref.get());
     }
 
-    @DevSdkIgnoreRule.IgnoreAfter(VERSION_CODES.UPSIDE_DOWN_CAKE)
-    @Test
-    public void testDataSaverStatusTracker() {
-        mockService(NetworkPolicyManager.class, Context.NETWORK_POLICY_SERVICE, mNpm);
-        // Mock proper application info.
-        doReturn(mCtx).when(mCtx).getApplicationContext();
-        final ApplicationInfo mockAppInfo = new ApplicationInfo();
-        mockAppInfo.flags = FLAG_PERSISTENT | FLAG_SYSTEM;
-        doReturn(mockAppInfo).when(mCtx).getApplicationInfo();
-        // Enable data saver.
-        doReturn(RESTRICT_BACKGROUND_STATUS_ENABLED).when(mNpm)
-                .getRestrictBackgroundStatus(anyInt());
-
-        final DataSaverStatusTracker tracker = new DataSaverStatusTracker(mCtx);
-        // Verify the data saver status is correct right after initialization.
-        assertTrue(tracker.getDataSaverEnabled());
-
-        // Verify the tracker register receiver with expected intent filter.
-        final ArgumentCaptor<IntentFilter> intentFilterCaptor =
-                ArgumentCaptor.forClass(IntentFilter.class);
-        verify(mCtx).registerReceiver(
-                any(), intentFilterCaptor.capture(), eq(RECEIVER_NOT_EXPORTED));
-        assertEquals(ACTION_RESTRICT_BACKGROUND_CHANGED,
-                intentFilterCaptor.getValue().getAction(0));
-
-        // Mock data saver status changed event and verify the tracker tracks the
-        // status accordingly.
-        doReturn(RESTRICT_BACKGROUND_STATUS_DISABLED).when(mNpm)
-                .getRestrictBackgroundStatus(anyInt());
-        tracker.onReceive(mCtx, new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED));
-        assertFalse(tracker.getDataSaverEnabled());
-
-        doReturn(RESTRICT_BACKGROUND_STATUS_WHITELISTED).when(mNpm)
-                .getRestrictBackgroundStatus(anyInt());
-        tracker.onReceive(mCtx, new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED));
-        assertTrue(tracker.getDataSaverEnabled());
-    }
-
     private <T> void mockService(Class<T> clazz, String name, T service) {
         doReturn(service).when(mCtx).getSystemService(name);
         doReturn(name).when(mCtx).getSystemServiceName(clazz);
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
new file mode 100644
index 0000000..c26ec53
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 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
+
+import android.content.Intent
+import android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.visibleOnHandlerThread
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.S_V2) // Bpf only supports in T+.
+class CSBpfNetMapsTest : CSTest() {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+
+    @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testCSTrackDataSaverBeforeV() {
+        val inOrder = inOrder(bpfNetMaps)
+        mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_WHITELISTED)
+        inOrder.verify(bpfNetMaps).setDataSaverEnabled(true)
+        mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_DISABLED)
+        inOrder.verify(bpfNetMaps).setDataSaverEnabled(false)
+        mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_ENABLED)
+        inOrder.verify(bpfNetMaps).setDataSaverEnabled(true)
+    }
+
+    // Data Saver Status is updated from platform code in V+.
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testCSTrackDataSaverAboveU() {
+        listOf(RESTRICT_BACKGROUND_STATUS_WHITELISTED, RESTRICT_BACKGROUND_STATUS_ENABLED,
+            RESTRICT_BACKGROUND_STATUS_DISABLED).forEach {
+            mockDataSaverStatus(it)
+            verify(bpfNetMaps, never()).setDataSaverEnabled(anyBoolean())
+        }
+    }
+
+    private fun mockDataSaverStatus(status: Int) {
+        doReturn(status).`when`(context.networkPolicyManager).getRestrictBackgroundStatus(anyInt())
+        // While the production code dispatches the intent on the handler thread,
+        // The test would dispatch the intent in the caller thread. Make it dispatch
+        // on the handler thread to match production behavior.
+        visibleOnHandlerThread(csHandler) {
+            context.sendBroadcast(Intent(ACTION_RESTRICT_BACKGROUND_CHANGED))
+        }
+        waitForIdle()
+    }
+}