Merge "[BR06] Support check whether network is blocked by data saver" into main
diff --git a/framework/src/android/net/BpfNetMapsReader.java b/framework/src/android/net/BpfNetMapsReader.java
index 9bce9cd..37c58f0 100644
--- a/framework/src/android/net/BpfNetMapsReader.java
+++ b/framework/src/android/net/BpfNetMapsReader.java
@@ -17,6 +17,8 @@
 package android.net;
 
 import static android.net.BpfNetMapsConstants.CONFIGURATION_MAP_PATH;
+import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
+import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
 import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
 import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
 import static android.net.BpfNetMapsUtils.getMatchByFirewallChain;
@@ -213,13 +215,16 @@
      * Return whether the network is blocked by firewall chains for the given uid.
      *
      * @param uid The target uid.
+     * @param isNetworkMetered Whether the target network is metered.
+     * @param isDataSaverEnabled Whether the data saver is enabled.
      *
      * @return True if the network is blocked. Otherwise, false.
      * @throws ServiceSpecificException if the read fails.
      *
      * @hide
      */
-    public boolean isUidBlockedByFirewallChains(final int uid) {
+    public boolean isUidNetworkingBlocked(final int uid, boolean isNetworkMetered,
+            boolean isDataSaverEnabled) {
         throwIfPreT("isUidBlockedByFirewallChains is not available on pre-T devices");
 
         final long uidRuleConfig;
@@ -235,6 +240,13 @@
 
         final boolean blockedByAllowChains = 0 != (uidRuleConfig & ~uidMatch & sMaskDropIfUnset);
         final boolean blockedByDenyChains = 0 != (uidRuleConfig & uidMatch & sMaskDropIfSet);
-        return blockedByAllowChains || blockedByDenyChains;
+        if (blockedByAllowChains || blockedByDenyChains) {
+            return true;
+        }
+
+        if (!isNetworkMetered) return false;
+        if ((uidMatch & PENALTY_BOX_MATCH) != 0) return true;
+        if ((uidMatch & HAPPY_BOX_MATCH) != 0) return false;
+        return isDataSaverEnabled;
     }
 }
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index ad76012..f44fd0e 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -16,6 +16,8 @@
 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;
@@ -25,6 +27,8 @@
 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;
@@ -37,12 +41,16 @@
 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;
@@ -74,6 +82,7 @@
 import android.util.SparseIntArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 
 import libcore.net.event.NetworkEventDispatcher;
 
@@ -95,6 +104,7 @@
 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
@@ -6199,13 +6209,99 @@
     }
 
     /**
-     * Return whether the network is blocked for the given uid.
+     * 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
      * maps and therefore considerably faster. For use by the NetworkStack process only.
      *
      * @param uid The target uid.
-     * @return True if all networking is blocked. Otherwise, false.
+     * @param isNetworkMetered Whether the target network is metered.
+     *
+     * @return True if all networking with the given condition is blocked. Otherwise, false.
      * @throws IllegalStateException if the map cannot be opened.
      * @throws ServiceSpecificException if the read fails.
      * @hide
@@ -6219,13 +6315,16 @@
     // @SystemApi(client = MODULE_LIBRARIES)
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)  // BPF maps were only mainlined in T
     @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
-    public boolean isUidNetworkingBlocked(int uid) {
+    public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
         final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
 
-        return reader.isUidBlockedByFirewallChains(uid);
+        final boolean isDataSaverEnabled;
+        // TODO: For U-QPR3+ devices, get data saver status from bpf configuration map directly.
+        final DataSaverStatusTracker dataSaverStatusTracker =
+                DataSaverStatusTracker.getInstance(mContext);
+        isDataSaverEnabled = dataSaverStatusTracker.getDataSaverEnabled();
 
-        // TODO: If isNetworkMetered is true, check the data saver switch, penalty box
-        //  and happy box rules.
+        return reader.isUidNetworkingBlocked(uid, isNetworkMetered, isDataSaverEnabled);
     }
 
     /** @hide */
diff --git a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
index 86a0acb..258e422 100644
--- a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
+++ b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
@@ -17,6 +17,8 @@
 package android.net
 
 import android.net.BpfNetMapsConstants.DOZABLE_MATCH
+import android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH
+import android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH
 import android.net.BpfNetMapsConstants.STANDBY_MATCH
 import android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY
 import android.net.BpfNetMapsUtils.getMatchByFirewallChain
@@ -36,6 +38,7 @@
 
 private const val TEST_UID1 = 1234
 private const val TEST_UID2 = TEST_UID1 + 1
+private const val TEST_UID3 = TEST_UID2 + 1
 private const val NO_IIF = 0
 
 // pre-T devices does not support Bpf.
@@ -101,23 +104,26 @@
         testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(newConfig))
     }
 
+    fun isUidNetworkingBlocked(uid: Int, metered: Boolean = false, dataSaver: Boolean = false) =
+            bpfNetMapsReader.isUidNetworkingBlocked(uid, metered, dataSaver)
+
     @Test
     fun testIsUidNetworkingBlockedByFirewallChains_allowChain() {
         // With everything disabled by default, verify the return value is false.
         testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
-        assertFalse(bpfNetMapsReader.isUidBlockedByFirewallChains(TEST_UID1))
+        assertFalse(isUidNetworkingBlocked(TEST_UID1))
 
         // Enable dozable chain but does not provide allowed list. Verify the network is blocked
         // for all uids.
         mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE, true)
-        assertTrue(bpfNetMapsReader.isUidBlockedByFirewallChains(TEST_UID1))
-        assertTrue(bpfNetMapsReader.isUidBlockedByFirewallChains(TEST_UID2))
+        assertTrue(isUidNetworkingBlocked(TEST_UID1))
+        assertTrue(isUidNetworkingBlocked(TEST_UID2))
 
         // Add uid1 to dozable allowed list. Verify the network is not blocked for uid1, while
         // uid2 is blocked.
         testUidOwnerMap.updateEntry(S32(TEST_UID1), UidOwnerValue(NO_IIF, DOZABLE_MATCH))
-        assertFalse(bpfNetMapsReader.isUidBlockedByFirewallChains(TEST_UID1))
-        assertTrue(bpfNetMapsReader.isUidBlockedByFirewallChains(TEST_UID2))
+        assertFalse(isUidNetworkingBlocked(TEST_UID1))
+        assertTrue(isUidNetworkingBlocked(TEST_UID2))
     }
 
     @Test
@@ -126,14 +132,14 @@
         // for all uids.
         testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
         mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_STANDBY, true)
-        assertFalse(bpfNetMapsReader.isUidBlockedByFirewallChains(TEST_UID1))
-        assertFalse(bpfNetMapsReader.isUidBlockedByFirewallChains(TEST_UID2))
+        assertFalse(isUidNetworkingBlocked(TEST_UID1))
+        assertFalse(isUidNetworkingBlocked(TEST_UID2))
 
         // Add uid1 to standby allowed list. Verify the network is blocked for uid1, while
         // uid2 is not blocked.
         testUidOwnerMap.updateEntry(S32(TEST_UID1), UidOwnerValue(NO_IIF, STANDBY_MATCH))
-        assertTrue(bpfNetMapsReader.isUidBlockedByFirewallChains(TEST_UID1))
-        assertFalse(bpfNetMapsReader.isUidBlockedByFirewallChains(TEST_UID2))
+        assertTrue(isUidNetworkingBlocked(TEST_UID1))
+        assertFalse(isUidNetworkingBlocked(TEST_UID2))
     }
 
     @Test
@@ -143,6 +149,54 @@
         testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
         mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_POWERSAVE, true)
         mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_STANDBY, true)
-        assertTrue(bpfNetMapsReader.isUidBlockedByFirewallChains(TEST_UID1))
+        assertTrue(isUidNetworkingBlocked(TEST_UID1))
+    }
+
+    @IgnoreUpTo(VERSION_CODES.S_V2)
+    @Test
+    fun testIsUidNetworkingBlockedByDataSaver() {
+        // With everything disabled by default, verify the return value is false.
+        testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
+        assertFalse(isUidNetworkingBlocked(TEST_UID1, metered = true))
+
+        // Add uid1 to penalty box, verify the network is blocked for uid1, while uid2 is not
+        // affected.
+        testUidOwnerMap.updateEntry(S32(TEST_UID1), UidOwnerValue(NO_IIF, PENALTY_BOX_MATCH))
+        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+        assertFalse(isUidNetworkingBlocked(TEST_UID2, metered = true))
+
+        // Enable data saver, verify the network is blocked for uid1, uid2, but uid3 in happy box
+        // is not affected.
+        testUidOwnerMap.updateEntry(S32(TEST_UID3), UidOwnerValue(NO_IIF, HAPPY_BOX_MATCH))
+        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true, dataSaver = true))
+        assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true, dataSaver = true))
+        assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true, dataSaver = true))
+
+        // Add uid1 to happy box as well, verify nothing is changed because penalty box has higher
+        // priority.
+        testUidOwnerMap.updateEntry(
+            S32(TEST_UID1),
+            UidOwnerValue(NO_IIF, PENALTY_BOX_MATCH or HAPPY_BOX_MATCH)
+        )
+        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true, dataSaver = true))
+        assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true, dataSaver = true))
+        assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true, dataSaver = true))
+
+        // Enable doze mode, verify uid3 is blocked even if it is in happy box.
+        mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE, true)
+        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true, dataSaver = true))
+        assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true, dataSaver = true))
+        assertTrue(isUidNetworkingBlocked(TEST_UID3, metered = true, dataSaver = true))
+
+        // Disable doze mode and data saver, only uid1 which is in penalty box is blocked.
+        mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE, false)
+        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+        assertFalse(isUidNetworkingBlocked(TEST_UID2, metered = true))
+        assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true))
+
+        // Make the network non-metered, nothing is blocked.
+        assertFalse(isUidNetworkingBlocked(TEST_UID1))
+        assertFalse(isUidNetworkingBlocked(TEST_UID2))
+        assertFalse(isUidNetworkingBlocked(TEST_UID3))
     }
 }
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index 45a9dbc..b8c5447 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -16,6 +16,13 @@
 
 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;
@@ -39,6 +46,7 @@
 
 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;
@@ -51,6 +59,7 @@
 import static org.mockito.Mockito.after;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -61,7 +70,10 @@
 
 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;
@@ -95,6 +107,7 @@
 
     @Mock Context mCtx;
     @Mock IConnectivityManager mService;
+    @Mock NetworkPolicyManager mNpm;
 
     @Before
     public void setUp() {
@@ -510,4 +523,54 @@
         assertNull("ConnectivityManager weak reference still not null after " + attempts
                     + " attempts", ref.get());
     }
+
+    @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);
+
+        // If the test suite uses the inline mock maker library, such as for coverage tests,
+        // then the final version of getSystemService must also be mocked, as the real
+        // method will not be called by the test and null object is returned since no mock.
+        // Otherwise, mocking a final method will fail the test.
+        if (mCtx.getSystemService(clazz) == null) {
+            doReturn(service).when(mCtx).getSystemService(clazz);
+        }
+    }
 }