Add compat flag for ActiveNetworkInfo blocked state

Before aosp/3001087, getNetworkActiveInfo did not return network info
with blocked state for apps without PERMISSION_INTERNET even if the
network is actually blocked.
aosp/3001087 updated getNetworkActiveInfo to return correct blocked
state on V+.
However, this broke some apps that use getNetworkActiveInfo in wrong way
and depends on the previous behavior.
So this CL introduces the compat flag to keep previous behavior for apps
targeting U or older releases.

Test: TH
Bug: 333340911
Change-Id: I0e0950f2148de0ffdb45495903a5a87e0d4c1c0b
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index a80db85..56537d9 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -99,6 +99,25 @@
     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public static final long ENABLE_MATCH_LOCAL_NETWORK = 319212206L;
 
+    /**
+     * On Android {@link android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM} or higher releases, when
+     * apps targeting Android {@link android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM} or higher
+     * that do not have the {@link android.Manifest.permission#INTERNET} permission call
+     * {@link android.net.ConnectivityManager#getActiveNetworkInfo()}, the state of the returned
+     * {@link android.net.NetworkInfo} object will always be
+     * {@link android.net.NetworkInfo.DetailedState#BLOCKED}. This is because apps without the
+     * permission cannot access any network.
+     * <p>
+     * For backwards compatibility, apps running on older releases, or targeting older SDK levels,
+     * will instead receive objects with the network's current state,
+     * such as {@link android.net.NetworkInfo.DetailedState#CONNECTED}.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static final long NETWORKINFO_WITHOUT_INTERNET_BLOCKED = 333340911L;
+
     private ConnectivityCompatChanges() {
     }
 }
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index c9108c3..23af0f8 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -806,6 +806,25 @@
     }
 
     /**
+     * Get granted permissions for specified uid. If uid is not in the map, this method returns
+     * {@link android.net.INetd.PERMISSION_INTERNET} since this is a default permission.
+     * See {@link #setNetPermForUids}
+     *
+     * @param uid target uid
+     * @return    granted permissions.
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public int getNetPermForUid(final int uid) {
+        try {
+            final U8 permissions = sUidPermissionMap.getValue(new S32(uid));
+            return permissions != null ? permissions.val : PERMISSION_INTERNET;
+        } catch (ErrnoException e) {
+            Log.wtf(TAG, "Failed to get permission for uid: " + uid);
+            return PERMISSION_INTERNET;
+        }
+    }
+
+    /**
      * Set Data Saver enabled or disabled
      *
      * @param enable     whether Data Saver is enabled or disabled.
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 637f794..46bd9bc 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -64,6 +64,7 @@
 import static android.net.ConnectivityManager.getNetworkTypeName;
 import static android.net.ConnectivityManager.isNetworkTypeValid;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_SKIPPED;
@@ -101,6 +102,7 @@
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST_ONLY;
 import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_LOCAL_NETWORK;
 import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_SELF_CERTIFIED_CAPABILITIES_DECLARATION;
+import static android.net.connectivity.ConnectivityCompatChanges.NETWORKINFO_WITHOUT_INTERNET_BLOCKED;
 import static android.os.Process.INVALID_UID;
 import static android.os.Process.VPN_UID;
 import static android.system.OsConstants.ETH_P_ALL;
@@ -2241,7 +2243,8 @@
         try {
             final boolean metered = nc == null ? true : nc.isMetered();
             if (mDeps.isAtLeastV()) {
-                return mBpfNetMaps.isUidNetworkingBlocked(uid, metered);
+                return mBpfNetMaps.isUidNetworkingBlocked(uid, metered)
+                        || (mBpfNetMaps.getNetPermForUid(uid) & PERMISSION_INTERNET) == 0;
             } else {
                 return mPolicyManager.isUidNetworkingBlocked(uid, metered);
             }
@@ -2322,7 +2325,12 @@
         final int uid = mDeps.getCallingUid();
         final NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid);
         if (nai == null) return null;
-        final NetworkInfo networkInfo = getFilteredNetworkInfo(nai, uid, false);
+        // Ignore blocked state to keep the backward compatibility if the compat flag is
+        // disabled and app does not have PERMISSION_INTERNET.
+        final boolean ignoreBlocked = mDeps.isAtLeastV()
+                && !mDeps.isChangeEnabled(NETWORKINFO_WITHOUT_INTERNET_BLOCKED, uid)
+                && (mBpfNetMaps.getNetPermForUid(uid) & PERMISSION_INTERNET) == 0;
+        final NetworkInfo networkInfo = getFilteredNetworkInfo(nai, uid, ignoreBlocked);
         maybeLogBlockedNetworkInfo(networkInfo, uid);
         return networkInfo;
     }
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index 62e55d5..cbc060a 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -848,6 +848,21 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testGetNetPermFoUid() throws Exception {
+        mUidPermissionMap.deleteEntry(new S32(TEST_UID));
+        assertEquals(PERMISSION_INTERNET, mBpfNetMaps.getNetPermForUid(TEST_UID));
+
+        mUidPermissionMap.updateEntry(new S32(TEST_UID), new U8((short) PERMISSION_NONE));
+        assertEquals(PERMISSION_NONE, mBpfNetMaps.getNetPermForUid(TEST_UID));
+
+        mUidPermissionMap.updateEntry(new S32(TEST_UID),
+                new U8((short) (PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS)));
+        assertEquals(PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS,
+                mBpfNetMaps.getNetPermForUid(TEST_UID));
+    }
+
+    @Test
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testSwapActiveStatsMap() throws Exception {
         mConfigurationMap.updateEntry(
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 878c7ff..717c5a1 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -87,6 +87,7 @@
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_FALLBACK;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP;
@@ -151,6 +152,7 @@
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_UNINITIALIZED;
 import static android.net.Proxy.PROXY_CHANGE_ACTION;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
+import static android.net.connectivity.ConnectivityCompatChanges.NETWORKINFO_WITHOUT_INTERNET_BLOCKED;
 import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_ADDED;
 import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_REMOVED;
 import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE;
@@ -1945,6 +1947,9 @@
         setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT);
         setAlwaysOnNetworks(false);
         setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com");
+
+        mDeps.setChangeIdEnabled(true, NETWORKINFO_WITHOUT_INTERNET_BLOCKED, Process.myUid());
+        doReturn(PERMISSION_INTERNET).when(mBpfNetMaps).getNetPermForUid(anyInt());
         // Note : Please do not add any new instrumentation here. If you need new instrumentation,
         // please add it in CSTest and use subclasses of CSTest instead of adding more
         // tools in ConnectivityServiceTest.
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSActiveNetworkInfoTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSActiveNetworkInfoTest.kt
new file mode 100644
index 0000000..1891a78
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSActiveNetworkInfoTest.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 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.net.INetd.PERMISSION_INTERNET
+import android.net.INetd.PERMISSION_NONE
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkInfo.DetailedState.BLOCKED
+import android.net.NetworkInfo.DetailedState.CONNECTED
+import android.net.connectivity.ConnectivityCompatChanges.NETWORKINFO_WITHOUT_INTERNET_BLOCKED
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doReturn
+
+private fun nc() = NetworkCapabilities.Builder()
+        .addTransportType(TRANSPORT_WIFI)
+        .addCapability(NET_CAPABILITY_INTERNET)
+        .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        .build()
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class CSActiveNetworkInfoTest : CSTest() {
+
+    fun doTestGetActiveNetworkInfo(
+            changeEnabled: Boolean,
+            permissions: Int,
+            expectBlocked: Boolean
+    ) {
+        deps.setChangeIdEnabled(changeEnabled, NETWORKINFO_WITHOUT_INTERNET_BLOCKED)
+        doReturn(permissions).`when`(bpfNetMaps).getNetPermForUid(anyInt())
+
+        val agent = Agent(nc = nc())
+        agent.connect()
+
+        val networkInfo = cm.activeNetworkInfo
+        assertNotNull(networkInfo)
+        if (expectBlocked) {
+            assertEquals(BLOCKED, networkInfo.detailedState)
+        } else {
+            assertEquals(CONNECTED, networkInfo.detailedState)
+        }
+        agent.disconnect()
+    }
+
+    @Test
+    fun testGetActiveNetworkInfo() {
+        doReturn(true).`when`(bpfNetMaps).isUidNetworkingBlocked(anyInt(), anyBoolean())
+        doTestGetActiveNetworkInfo(
+                changeEnabled = true,
+                permissions = PERMISSION_NONE,
+                expectBlocked = true
+        )
+        doTestGetActiveNetworkInfo(
+                changeEnabled = false,
+                permissions = PERMISSION_INTERNET,
+                expectBlocked = true
+        )
+        // getActiveNetworkInfo does not return NetworkInfo with blocked state if the compat change
+        // is disabled and the app does not have PERMISSION_INTERNET
+        doTestGetActiveNetworkInfo(
+                changeEnabled = false,
+                permissions = PERMISSION_NONE,
+                expectBlocked = false
+        )
+    }
+}