Merge "Merge ab/7633965" into stage-aosp-sc-ts-dev
diff --git a/service/Android.bp b/service/Android.bp
index 7fe0e2b..39f970d 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -65,7 +65,7 @@
         "ServiceConnectivityResources",
     ],
     static_libs: [
-        "dnsresolver_aidl_interface-V8-java",
+        "dnsresolver_aidl_interface-V9-java",
         "modules-utils-os",
         "net-utils-device-common",
         "net-utils-framework-common",
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index bf32ad5..b22457a 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -114,4 +114,15 @@
     <!-- Whether to cancel network notifications automatically when tapped -->
     <bool name="config_autoCancelNetworkNotifications">true</bool>
 
+    <!-- When no internet or partial connectivity is detected on a network, and a high priority
+         (heads up) notification would be shown due to the network being explicitly selected,
+         directly show the dialog that would normally be shown when tapping the notification
+         instead of showing the notification. -->
+    <bool name="config_notifyNoInternetAsDialogWhenHighPriority">false</bool>
+
+    <!-- When showing notifications indicating partial connectivity, display the same notifications
+         as no connectivity instead. This may be easier to understand for users but offers less
+         details on what is happening. -->
+    <bool name="config_partialConnectivityNotifiedAsNoInternet">false</bool>
+
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 6ac6a0e..5af13d7 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -32,6 +32,8 @@
             <item type="array" name="config_networkNotifySwitches"/>
             <item type="bool" name="config_ongoingSignInNotification"/>
             <item type="bool" name="config_autoCancelNetworkNotifications"/>
+            <item type="bool" name="config_notifyNoInternetAsDialogWhenHighPriority"/>
+            <item type="bool" name="config_partialConnectivityNotifiedAsNoInternet"/>
             <item type="drawable" name="stat_notify_wifi_in_range"/>
             <item type="drawable" name="stat_notify_rssi_in_range"/>
         </policy>
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index c8a0b7f..e34c064 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -324,7 +324,8 @@
     private static final int DEFAULT_NASCENT_DELAY_MS = 5_000;
 
     // The maximum number of network request allowed per uid before an exception is thrown.
-    private static final int MAX_NETWORK_REQUESTS_PER_UID = 100;
+    @VisibleForTesting
+    static final int MAX_NETWORK_REQUESTS_PER_UID = 100;
 
     // The maximum number of network request allowed for system UIDs before an exception is thrown.
     @VisibleForTesting
@@ -344,7 +345,8 @@
     @VisibleForTesting
     protected final PermissionMonitor mPermissionMonitor;
 
-    private final PerUidCounter mNetworkRequestCounter;
+    @VisibleForTesting
+    final PerUidCounter mNetworkRequestCounter;
     @VisibleForTesting
     final PerUidCounter mSystemNetworkRequestCounter;
 
@@ -1154,9 +1156,20 @@
         private void incrementCountOrThrow(final int uid, final int numToIncrement) {
             final int newRequestCount =
                     mUidToNetworkRequestCount.get(uid, 0) + numToIncrement;
-            if (newRequestCount >= mMaxCountPerUid) {
+            if (newRequestCount >= mMaxCountPerUid
+                    // HACK : the system server is allowed to go over the request count limit
+                    // when it is creating requests on behalf of another app (but not itself,
+                    // so it can still detect its own request leaks). This only happens in the
+                    // per-app API flows in which case the old requests for that particular
+                    // UID will be removed soon.
+                    // TODO : instead of this hack, addPerAppDefaultNetworkRequests and other
+                    // users of transact() should unregister the requests to decrease the count
+                    // before they increase it again by creating a new NRI. Then remove the
+                    // transact() method.
+                    && (Process.myUid() == uid || Process.myUid() != Binder.getCallingUid())) {
                 throw new ServiceSpecificException(
-                        ConnectivityManager.Errors.TOO_MANY_REQUESTS);
+                        ConnectivityManager.Errors.TOO_MANY_REQUESTS,
+                        "Uid " + uid + " exceeded its allotted requests limit");
             }
             mUidToNetworkRequestCount.put(uid, newRequestCount);
         }
@@ -5817,7 +5830,7 @@
             mUid = nri.mUid;
             mAsUid = nri.mAsUid;
             mPendingIntent = nri.mPendingIntent;
-            mPerUidCounter = getRequestCounter(this);
+            mPerUidCounter = nri.mPerUidCounter;
             mPerUidCounter.incrementCountOrThrow(mUid);
             mCallbackFlags = nri.mCallbackFlags;
             mCallingAttributionTag = nri.mCallingAttributionTag;
@@ -10147,7 +10160,7 @@
             final NetworkRequestInfo trackingNri =
                     getDefaultRequestTrackingUid(callbackRequest.mAsUid);
 
-            // If this nri is not being tracked, the change it back to an untracked nri.
+            // If this nri is not being tracked, then change it back to an untracked nri.
             if (trackingNri == mDefaultRequest) {
                 callbackRequestsToRegister.add(new NetworkRequestInfo(
                         callbackRequest,
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
index 05b12ba..1493cae 100644
--- a/service/src/com/android/server/connectivity/DnsManager.java
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -38,7 +38,6 @@
 import android.net.InetAddresses;
 import android.net.LinkProperties;
 import android.net.Network;
-import android.net.ResolverOptionsParcel;
 import android.net.ResolverParamsParcel;
 import android.net.Uri;
 import android.net.shared.PrivateDnsConfig;
@@ -384,7 +383,6 @@
                               .collect(Collectors.toList()))
                 : useTls ? paramsParcel.servers  // Opportunistic
                 : new String[0];            // Off
-        paramsParcel.resolverOptions = new ResolverOptionsParcel();
         paramsParcel.transportTypes = transportTypes;
         // Prepare to track the validation status of the DNS servers in the
         // resolver config when private DNS is in opportunistic or strict mode.
diff --git a/service/src/com/android/server/connectivity/FullScore.java b/service/src/com/android/server/connectivity/FullScore.java
index fbfa7a1..14cec09 100644
--- a/service/src/com/android/server/connectivity/FullScore.java
+++ b/service/src/com/android/server/connectivity/FullScore.java
@@ -108,10 +108,9 @@
     // and all bits managed by FullScore unset. As bits are handled from 0 up in NetworkScore and
     // from 63 down in FullScore, cut at the 32nd bit for simplicity, but change this if some day
     // there are more than 32 bits handled on either side.
-    // YIELD_TO_BAD_WIFI is temporarily handled by ConnectivityService, but the factory is still
-    // allowed to set it, so that it's possible to transition from handling it in CS to handling
-    // it in the factory.
-    private static final long EXTERNAL_POLICIES_MASK = 0x00000000FFFFFFFFL;
+    // YIELD_TO_BAD_WIFI is temporarily handled by ConnectivityService.
+    private static final long EXTERNAL_POLICIES_MASK =
+            0x00000000FFFFFFFFL & ~(1L << POLICY_YIELD_TO_BAD_WIFI);
 
     @VisibleForTesting
     static @NonNull String policyNameOf(final int policy) {
diff --git a/service/src/com/android/server/connectivity/NetworkNotificationManager.java b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
index ae98d92..155f6c4 100644
--- a/service/src/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
@@ -198,11 +198,22 @@
         }
 
         final Resources r = mResources.get();
+        if (highPriority && maybeNotifyViaDialog(r, notifyType, intent)) {
+            Log.d(TAG, "Notified via dialog for event " + nameOf(eventId));
+            return;
+        }
+
         final CharSequence title;
         final CharSequence details;
         Icon icon = Icon.createWithResource(
                 mResources.getResourcesContext(), getIcon(transportType));
-        if (notifyType == NotificationType.NO_INTERNET && transportType == TRANSPORT_WIFI) {
+        final boolean showAsNoInternet = notifyType == NotificationType.PARTIAL_CONNECTIVITY
+                && r.getBoolean(R.bool.config_partialConnectivityNotifiedAsNoInternet);
+        if (showAsNoInternet) {
+            Log.d(TAG, "Showing partial connectivity as NO_INTERNET");
+        }
+        if ((notifyType == NotificationType.NO_INTERNET || showAsNoInternet)
+                && transportType == TRANSPORT_WIFI) {
             title = r.getString(R.string.wifi_no_internet, name);
             details = r.getString(R.string.wifi_no_internet_detailed);
         } else if (notifyType == NotificationType.PRIVATE_DNS_BROKEN) {
@@ -306,6 +317,24 @@
         }
     }
 
+    private boolean maybeNotifyViaDialog(Resources res, NotificationType notifyType,
+            PendingIntent intent) {
+        if (notifyType != NotificationType.NO_INTERNET
+                && notifyType != NotificationType.PARTIAL_CONNECTIVITY) {
+            return false;
+        }
+        if (!res.getBoolean(R.bool.config_notifyNoInternetAsDialogWhenHighPriority)) {
+            return false;
+        }
+
+        try {
+            intent.send();
+        } catch (PendingIntent.CanceledException e) {
+            Log.e(TAG, "Error sending dialog PendingIntent", e);
+        }
+        return true;
+    }
+
     /**
      * Clear the notification with the given id, only if it matches the given type.
      */
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index ccbdbd3..60a20f4 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -22,6 +22,7 @@
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_ATTEMPTED_BITMASK;
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_SUCCEEDED_BITMASK;
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_VALIDATION_RESULT;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.NETWORK_VALIDATION_RESULT_SKIPPED;
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.NETWORK_VALIDATION_RESULT_VALID;
 import static android.net.ConnectivityDiagnosticsManager.DataStallReport;
 import static android.net.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_DNS_EVENTS;
@@ -78,6 +79,7 @@
 
 import com.android.internal.telephony.uicc.IccUtils;
 import com.android.internal.util.ArrayUtils;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.ArrayTrackRecord;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
@@ -427,6 +429,12 @@
         // revalidated which will trigger another onConnectivityReportAvailable callback.
         if (!hasConnectivity) {
             cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName);
+        } else if (SdkLevel.isAtLeastS()) {
+            // All calls to #onNetworkConnectivityReported are expected to be accompanied by a call
+            // to #onConnectivityReportAvailable after a mainline update in the S timeframe.
+            // Optionally validate this, but do not fail if it does not exist.
+            cb.maybeVerifyOnConnectivityReportAvailable(mTestNetwork, interfaceName, TRANSPORT_TEST,
+                    false /* requireCallbackFired */);
         }
 
         cb.assertNoCallback();
@@ -479,13 +487,25 @@
 
         public void expectOnConnectivityReportAvailable(
                 @NonNull Network network, @NonNull String interfaceName) {
-            expectOnConnectivityReportAvailable(network, interfaceName, TRANSPORT_TEST);
+            expectOnConnectivityReportAvailable(
+                    network, interfaceName, TRANSPORT_TEST);
         }
 
-        public void expectOnConnectivityReportAvailable(
-                @NonNull Network network, @NonNull String interfaceName, int transportType) {
+        public void expectOnConnectivityReportAvailable(@NonNull Network network,
+                @NonNull String interfaceName, int transportType) {
+            maybeVerifyOnConnectivityReportAvailable(network, interfaceName, transportType,
+                    true /* requireCallbackFired */);
+        }
+
+        public void maybeVerifyOnConnectivityReportAvailable(@NonNull Network network,
+                @NonNull String interfaceName, int transportType, boolean requireCallbackFired) {
             final ConnectivityReport result =
                     (ConnectivityReport) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true);
+
+            // If callback is not required and there is no report, exit early.
+            if (!requireCallbackFired && result == null) {
+                return;
+            }
             assertEquals(network, result.getNetwork());
 
             final NetworkCapabilities nc = result.getNetworkCapabilities();
@@ -496,9 +516,16 @@
 
             final PersistableBundle extras = result.getAdditionalInfo();
             assertTrue(extras.containsKey(KEY_NETWORK_VALIDATION_RESULT));
-            final int validationResult = extras.getInt(KEY_NETWORK_VALIDATION_RESULT);
-            assertEquals("Network validation result is not 'valid'",
-                    NETWORK_VALIDATION_RESULT_VALID, validationResult);
+            final int actualValidationResult = extras.getInt(KEY_NETWORK_VALIDATION_RESULT);
+
+            // Allow RESULT_VALID for networks that are expected to be skipped. Android S shipped
+            // with validation results being reported as VALID, but the behavior will be updated via
+            // mainline update. Allow both behaviors, and let MTS enforce stricter behavior
+            if (actualValidationResult != NETWORK_VALIDATION_RESULT_SKIPPED
+                    && actualValidationResult != NETWORK_VALIDATION_RESULT_VALID) {
+                fail("Network validation result was incorrect; expected skipped or valid, but "
+                        + "got " + actualValidationResult);
+            }
 
             assertTrue(extras.containsKey(KEY_NETWORK_PROBES_SUCCEEDED_BITMASK));
             final int probesSucceeded = extras.getInt(KEY_NETWORK_VALIDATION_RESULT);
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 26f7f4a..7b5b44f 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -30,6 +30,7 @@
     ],
     libs: [
         "android.test.mock",
+        "ServiceConnectivityResources",
     ],
     static_libs: [
         "NetworkStackApiStableLib",
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index e039ef0..80338aa 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -23,7 +23,9 @@
 import android.content.Context.BIND_IMPORTANT
 import android.content.Intent
 import android.content.ServiceConnection
+import android.content.res.Resources
 import android.net.ConnectivityManager
+import android.net.ConnectivityResources
 import android.net.IDnsResolver
 import android.net.INetd
 import android.net.LinkProperties
@@ -35,6 +37,7 @@
 import android.net.TestNetworkStackClient
 import android.net.Uri
 import android.net.metrics.IpConnectivityLog
+import android.net.util.MultinetworkPolicyTracker
 import android.os.ConditionVariable
 import android.os.IBinder
 import android.os.SystemConfigManager
@@ -43,6 +46,7 @@
 import android.util.Log
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.connectivity.resources.R
 import com.android.server.ConnectivityService
 import com.android.server.NetworkAgentWrapper
 import com.android.server.TestNetIdManager
@@ -59,6 +63,7 @@
 import org.mockito.Mock
 import org.mockito.Mockito.any
 import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.doNothing
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.eq
@@ -93,6 +98,10 @@
     private lateinit var dnsResolver: IDnsResolver
     @Mock
     private lateinit var systemConfigManager: SystemConfigManager
+    @Mock
+    private lateinit var resources: Resources
+    @Mock
+    private lateinit var resourcesContext: Context
     @Spy
     private var context = TestableContext(realContext)
 
@@ -110,9 +119,11 @@
 
         private val realContext get() = InstrumentationRegistry.getInstrumentation().context
         private val httpProbeUrl get() =
-            realContext.getResources().getString(R.string.config_captive_portal_http_url)
+            realContext.getResources().getString(com.android.server.net.integrationtests.R.string
+                    .config_captive_portal_http_url)
         private val httpsProbeUrl get() =
-            realContext.getResources().getString(R.string.config_captive_portal_https_url)
+            realContext.getResources().getString(com.android.server.net.integrationtests.R.string
+                    .config_captive_portal_https_url)
 
         private class InstrumentationServiceConnection : ServiceConnection {
             override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
@@ -156,6 +167,27 @@
                 .getSystemService(Context.SYSTEM_CONFIG_SERVICE)
         doReturn(IntArray(0)).`when`(systemConfigManager).getSystemPermissionUids(anyString())
 
+        doReturn(60000).`when`(resources).getInteger(R.integer.config_networkTransitionTimeout)
+        doReturn("").`when`(resources).getString(R.string.config_networkCaptivePortalServerUrl)
+        doReturn(arrayOf<String>("test_wlan_wol")).`when`(resources)
+                .getStringArray(R.array.config_wakeonlan_supported_interfaces)
+        doReturn(arrayOf("0,1", "1,3")).`when`(resources)
+                .getStringArray(R.array.config_networkSupportedKeepaliveCount)
+        doReturn(emptyArray<String>()).`when`(resources)
+                .getStringArray(R.array.config_networkNotifySwitches)
+        doReturn(intArrayOf(10, 11, 12, 14, 15)).`when`(resources)
+                .getIntArray(R.array.config_protectedNetworks)
+        // We don't test the actual notification value strings, so just return an empty array.
+        // It doesn't matter what the values are as long as it's not null.
+        doReturn(emptyArray<String>()).`when`(resources).getStringArray(
+                R.array.network_switch_type_name)
+        doReturn(1).`when`(resources).getInteger(R.integer.config_networkAvoidBadWifi)
+        doReturn(R.array.config_networkSupportedKeepaliveCount).`when`(resources)
+                .getIdentifier(eq("config_networkSupportedKeepaliveCount"), eq("array"), any())
+
+        doReturn(resources).`when`(resourcesContext).getResources()
+        ConnectivityResources.setResourcesContextForTest(resourcesContext)
+
         networkStackClient = TestNetworkStackClient(realContext)
         networkStackClient.start()
 
@@ -176,12 +208,19 @@
         doReturn(mock(ProxyTracker::class.java)).`when`(deps).makeProxyTracker(any(), any())
         doReturn(mock(MockableSystemProperties::class.java)).`when`(deps).systemProperties
         doReturn(TestNetIdManager()).`when`(deps).makeNetIdManager()
+        doAnswer { inv ->
+            object : MultinetworkPolicyTracker(inv.getArgument(0), inv.getArgument(1),
+                    inv.getArgument(2)) {
+                override fun getResourcesForActiveSubId() = resources
+            }
+        }.`when`(deps).makeMultinetworkPolicyTracker(any(), any(), any())
         return deps
     }
 
     @After
     fun tearDown() {
         nsInstrumentation.clearAllState()
+        ConnectivityResources.setResourcesContextForTest(null)
     }
 
     @Test
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index c8476cb..71bd608 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -62,6 +62,7 @@
     jarjar_rules: "jarjar-rules.txt",
     static_libs: [
         "androidx.test.rules",
+        "androidx.test.uiautomator",
         "bouncycastle-repackaged-unbundled",
         "core-tests-support",
         "FrameworksNetCommonTests",
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index 4c60ccf..887f171 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -53,6 +53,8 @@
     <application>
         <uses-library android:name="android.test.runner" />
         <uses-library android:name="android.net.ipsec.ike" />
+        <activity
+            android:name="com.android.server.connectivity.NetworkNotificationManagerTest$TestDialogActivity"/>
     </application>
 
     <instrumentation
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 0f0ac12..10b7e14 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -125,6 +125,7 @@
 import static android.os.Process.INVALID_UID;
 import static android.system.OsConstants.IPPROTO_TCP;
 
+import static com.android.server.ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID;
 import static com.android.server.ConnectivityService.PREFERENCE_PRIORITY_MOBILE_DATA_PREFERERRED;
 import static com.android.server.ConnectivityService.PREFERENCE_PRIORITY_OEM;
 import static com.android.server.ConnectivityService.PREFERENCE_PRIORITY_PROFILE;
@@ -539,6 +540,9 @@
         private final LinkedBlockingQueue<Intent> mStartedActivities = new LinkedBlockingQueue<>();
 
         // Map of permission name -> PermissionManager.Permission_{GRANTED|DENIED} constant
+        // For permissions granted across the board, the key is only the permission name.
+        // For permissions only granted to a combination of uid/pid, the key
+        // is "<permission name>,<pid>,<uid>". PID+UID permissons have priority over generic ones.
         private final HashMap<String, Integer> mMockedPermissions = new HashMap<>();
 
         MockContext(Context base, ContentProvider settingsProvider) {
@@ -640,30 +644,40 @@
             return mPackageManager;
         }
 
-        private int checkMockedPermission(String permission, Supplier<Integer> ifAbsent) {
-            final Integer granted = mMockedPermissions.get(permission);
-            return granted != null ? granted : ifAbsent.get();
+        private int checkMockedPermission(String permission, int pid, int uid,
+                Supplier<Integer> ifAbsent) {
+            final Integer granted = mMockedPermissions.get(permission + "," + pid + "," + uid);
+            if (null != granted) {
+                return granted;
+            }
+            final Integer allGranted = mMockedPermissions.get(permission);
+            if (null != allGranted) {
+                return allGranted;
+            }
+            return ifAbsent.get();
         }
 
         @Override
         public int checkPermission(String permission, int pid, int uid) {
-            return checkMockedPermission(
-                    permission, () -> super.checkPermission(permission, pid, uid));
+            return checkMockedPermission(permission, pid, uid,
+                    () -> super.checkPermission(permission, pid, uid));
         }
 
         @Override
         public int checkCallingOrSelfPermission(String permission) {
-            return checkMockedPermission(
-                    permission, () -> super.checkCallingOrSelfPermission(permission));
+            return checkMockedPermission(permission, Process.myPid(), Process.myUid(),
+                    () -> super.checkCallingOrSelfPermission(permission));
         }
 
         @Override
         public void enforceCallingOrSelfPermission(String permission, String message) {
-            final Integer granted = mMockedPermissions.get(permission);
-            if (granted == null) {
-                super.enforceCallingOrSelfPermission(permission, message);
-                return;
-            }
+            final Integer granted = checkMockedPermission(permission,
+                    Process.myPid(), Process.myUid(),
+                    () -> {
+                        super.enforceCallingOrSelfPermission(permission, message);
+                        // enforce will crash if the permission is not granted
+                        return PERMISSION_GRANTED;
+                    });
 
             if (!granted.equals(PERMISSION_GRANTED)) {
                 throw new SecurityException("[Test] permission denied: " + permission);
@@ -673,6 +687,8 @@
         /**
          * Mock checks for the specified permission, and have them behave as per {@code granted}.
          *
+         * This will apply across the board no matter what the checked UID and PID are.
+         *
          * <p>Passing null reverts to default behavior, which does a real permission check on the
          * test package.
          * @param granted One of {@link PackageManager#PERMISSION_GRANTED} or
@@ -682,6 +698,21 @@
             mMockedPermissions.put(permission, granted);
         }
 
+        /**
+         * Mock checks for the specified permission, and have them behave as per {@code granted}.
+         *
+         * This will only apply to the passed UID and PID.
+         *
+         * <p>Passing null reverts to default behavior, which does a real permission check on the
+         * test package.
+         * @param granted One of {@link PackageManager#PERMISSION_GRANTED} or
+         *                {@link PackageManager#PERMISSION_DENIED}.
+         */
+        public void setPermission(String permission, int pid, int uid, Integer granted) {
+            final String key = permission + "," + pid + "," + uid;
+            mMockedPermissions.put(key, granted);
+        }
+
         @Override
         public Intent registerReceiverForAllUsers(@Nullable BroadcastReceiver receiver,
                 @NonNull IntentFilter filter, @Nullable String broadcastPermission,
@@ -1569,15 +1600,21 @@
     }
 
     private void withPermission(String permission, ExceptionalRunnable r) throws Exception {
-        if (mServiceContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
-            r.run();
-            return;
-        }
         try {
             mServiceContext.setPermission(permission, PERMISSION_GRANTED);
             r.run();
         } finally {
-            mServiceContext.setPermission(permission, PERMISSION_DENIED);
+            mServiceContext.setPermission(permission, null);
+        }
+    }
+
+    private void withPermission(String permission, int pid, int uid, ExceptionalRunnable r)
+            throws Exception {
+        try {
+            mServiceContext.setPermission(permission, pid, uid, PERMISSION_GRANTED);
+            r.run();
+        } finally {
+            mServiceContext.setPermission(permission, pid, uid, null);
         }
     }
 
@@ -9510,6 +9547,19 @@
     }
 
     @Test
+    public void testStartVpnProfileFromDiffPackage() throws Exception {
+        final String notMyVpnPkg = "com.not.my.vpn";
+        assertThrows(
+                SecurityException.class, () -> mVpnManagerService.startVpnProfile(notMyVpnPkg));
+    }
+
+    @Test
+    public void testStopVpnProfileFromDiffPackage() throws Exception {
+        final String notMyVpnPkg = "com.not.my.vpn";
+        assertThrows(SecurityException.class, () -> mVpnManagerService.stopVpnProfile(notMyVpnPkg));
+    }
+
+    @Test
     public void testUidUpdateChangesInterfaceFilteringRule() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
@@ -13368,17 +13418,45 @@
     @Test
     public void testProfileNetworkPrefCountsRequestsCorrectlyOnSet() throws Exception {
         final UserHandle testHandle = setupEnterpriseNetwork();
-        testRequestCountLimits(() -> {
-            // Set initially to test the limit prior to having existing requests.
-            final TestOnCompleteListener listener = new TestOnCompleteListener();
-            mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
-                    Runnable::run, listener);
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+        // Leave one request available so the profile preference can be set.
+        testRequestCountLimits(1 /* countToLeaveAvailable */, () -> {
+            withPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+                    Process.myPid(), Process.myUid(), () -> {
+                        // Set initially to test the limit prior to having existing requests.
+                        mCm.setProfileNetworkPreference(testHandle,
+                                PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
+                                Runnable::run, listener);
+                    });
             listener.expectOnComplete();
 
-            // re-set so as to test the limit as part of replacing existing requests.
-            mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
-                    Runnable::run, listener);
+            // Simulate filing requests as some app on the work profile
+            final int otherAppUid = UserHandle.getUid(TEST_WORK_PROFILE_USER_ID,
+                    UserHandle.getAppId(Process.myUid() + 1));
+            final int remainingCount = ConnectivityService.MAX_NETWORK_REQUESTS_PER_UID
+                    - mService.mNetworkRequestCounter.mUidToNetworkRequestCount.get(otherAppUid)
+                    - 1;
+            final NetworkCallback[] callbacks = new NetworkCallback[remainingCount];
+            doAsUid(otherAppUid, () -> {
+                for (int i = 0; i < remainingCount; ++i) {
+                    callbacks[i] = new TestableNetworkCallback();
+                    mCm.registerDefaultNetworkCallback(callbacks[i]);
+                }
+            });
+
+            withPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+                    Process.myPid(), Process.myUid(), () -> {
+                        // re-set so as to test the limit as part of replacing existing requests.
+                        mCm.setProfileNetworkPreference(testHandle,
+                                PROFILE_NETWORK_PREFERENCE_ENTERPRISE, Runnable::run, listener);
+                    });
             listener.expectOnComplete();
+
+            doAsUid(otherAppUid, () -> {
+                for (final NetworkCallback callback : callbacks) {
+                    mCm.unregisterNetworkCallback(callback);
+                }
+            });
         });
     }
 
@@ -13390,39 +13468,45 @@
         mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true);
         @OemNetworkPreferences.OemNetworkPreference final int networkPref =
                 OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY;
-        testRequestCountLimits(() -> {
-            // Set initially to test the limit prior to having existing requests.
-            final TestOemListenerCallback listener = new TestOemListenerCallback();
-            mService.setOemNetworkPreference(
-                    createDefaultOemNetworkPreferences(networkPref), listener);
-            listener.expectOnComplete();
+        // Leave one request available so the OEM preference can be set.
+        testRequestCountLimits(1 /* countToLeaveAvailable */, () ->
+                withPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, () -> {
+                    // Set initially to test the limit prior to having existing requests.
+                    final TestOemListenerCallback listener = new TestOemListenerCallback();
+                    mService.setOemNetworkPreference(
+                            createDefaultOemNetworkPreferences(networkPref), listener);
+                    listener.expectOnComplete();
 
-            // re-set so as to test the limit as part of replacing existing requests.
-            mService.setOemNetworkPreference(
-                    createDefaultOemNetworkPreferences(networkPref), listener);
-            listener.expectOnComplete();
-        });
+                    // re-set so as to test the limit as part of replacing existing requests.
+                    mService.setOemNetworkPreference(
+                            createDefaultOemNetworkPreferences(networkPref), listener);
+                    listener.expectOnComplete();
+                }));
     }
 
-    private void testRequestCountLimits(@NonNull final Runnable r) throws Exception {
+    private void testRequestCountLimits(final int countToLeaveAvailable,
+            @NonNull final ExceptionalRunnable r) throws Exception {
         final ArraySet<TestNetworkCallback> callbacks = new ArraySet<>();
         try {
             final int requestCount = mService.mSystemNetworkRequestCounter
                     .mUidToNetworkRequestCount.get(Process.myUid());
-            // The limit is hit when total requests <= limit.
-            final int maxCount =
-                    ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - requestCount;
+            // The limit is hit when total requests = limit - 1, and exceeded with a crash when
+            // total requests >= limit.
+            final int countToFile =
+                    MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - requestCount - countToLeaveAvailable;
             // Need permission so registerDefaultNetworkCallback uses mSystemNetworkRequestCounter
             withPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, () -> {
-                for (int i = 1; i < maxCount - 1; i++) {
+                for (int i = 1; i < countToFile; i++) {
                     final TestNetworkCallback cb = new TestNetworkCallback();
                     mCm.registerDefaultNetworkCallback(cb);
                     callbacks.add(cb);
                 }
-
-                // Code to run to check if it triggers a max request count limit error.
-                r.run();
+                assertEquals(MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - 1 - countToLeaveAvailable,
+                        mService.mSystemNetworkRequestCounter
+                              .mUidToNetworkRequestCount.get(Process.myUid()));
             });
+            // Code to run to check if it triggers a max request count limit error.
+            r.run();
         } finally {
             for (final TestNetworkCallback cb : callbacks) {
                 mCm.unregisterNetworkCallback(cb);
@@ -13667,15 +13751,18 @@
     public void testMobileDataPreferredUidsChangedCountsRequestsCorrectlyOnSet() throws Exception {
         ConnectivitySettingsManager.setMobileDataPreferredUids(mServiceContext,
                 Set.of(PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)));
-        testRequestCountLimits(() -> {
-            // Set initially to test the limit prior to having existing requests.
-            mService.updateMobileDataPreferredUids();
-            waitForIdle();
+        // Leave one request available so MDO preference set up above can be set.
+        testRequestCountLimits(1 /* countToLeaveAvailable */, () ->
+                withPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+                        Process.myPid(), Process.myUid(), () -> {
+                            // Set initially to test the limit prior to having existing requests.
+                            mService.updateMobileDataPreferredUids();
+                            waitForIdle();
 
-            // re-set so as to test the limit as part of replacing existing requests.
-            mService.updateMobileDataPreferredUids();
-            waitForIdle();
-        });
+                            // re-set so as to test the limit as part of replacing existing requests
+                            mService.updateMobileDataPreferredUids();
+                            waitForIdle();
+                        }));
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
index 9ef558f..24aecdb 100644
--- a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -43,6 +43,7 @@
 import static org.mockito.Mockito.when;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.net.ConnectivitySettingsManager;
 import android.net.IDnsResolver;
@@ -106,8 +107,14 @@
     @Mock IDnsResolver mMockDnsResolver;
 
     private void assertResolverOptionsEquals(
-            @NonNull ResolverOptionsParcel actual,
-            @NonNull ResolverOptionsParcel expected) {
+            @Nullable ResolverOptionsParcel actual,
+            @Nullable ResolverOptionsParcel expected) {
+        if (actual == null) {
+            assertNull(expected);
+            return;
+        } else {
+            assertNotNull(expected);
+        }
         assertEquals(actual.hosts, expected.hosts);
         assertEquals(actual.tcMode, expected.tcMode);
         assertEquals(actual.enforceDnsUid, expected.enforceDnsUid);
@@ -365,7 +372,7 @@
         expectedParams.tlsName = "";
         expectedParams.tlsServers = new String[]{"3.3.3.3", "4.4.4.4"};
         expectedParams.transportTypes = TEST_TRANSPORT_TYPES;
-        expectedParams.resolverOptions = new ResolverOptionsParcel();
+        expectedParams.resolverOptions = null;
         assertResolverParamsEquals(actualParams, expectedParams);
     }
 
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
index c1059b3..2cf5d8e 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -27,6 +27,7 @@
 import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.SIGN_IN;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.clearInvocations;
@@ -39,9 +40,14 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
@@ -49,11 +55,19 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.UserHandle;
 import android.telephony.TelephonyManager;
 import android.util.DisplayMetrics;
+import android.widget.TextView;
 
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject;
+import androidx.test.uiautomator.UiSelector;
 
 import com.android.connectivity.resources.R;
 import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
@@ -84,6 +98,7 @@
     private static final String TEST_EXTRA_INFO = "extra";
     private static final int TEST_NOTIF_ID = 101;
     private static final String TEST_NOTIF_TAG = NetworkNotificationManager.tagFor(TEST_NOTIF_ID);
+    private static final long TEST_TIMEOUT_MS = 10_000L;
     static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities();
     static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities();
     static final NetworkCapabilities VPN_CAPABILITIES = new NetworkCapabilities();
@@ -102,6 +117,25 @@
         VPN_CAPABILITIES.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
     }
 
+    /**
+     * Test activity that shows the action it was started with on screen, and dismisses when the
+     * text is tapped.
+     */
+    public static class TestDialogActivity extends Activity {
+        @Override
+        protected void onCreate(@Nullable Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setTurnScreenOn(true);
+            getSystemService(KeyguardManager.class).requestDismissKeyguard(
+                    this, null /* callback */);
+
+            final TextView txt = new TextView(this);
+            txt.setText(getIntent().getAction());
+            txt.setOnClickListener(e -> finish());
+            setContentView(txt);
+        }
+    }
+
     @Mock Context mCtx;
     @Mock Resources mResources;
     @Mock DisplayMetrics mDisplayMetrics;
@@ -345,4 +379,82 @@
         mManager.clearNotification(id, PARTIAL_CONNECTIVITY);
         verify(mNotificationManager, never()).cancel(eq(tag), eq(PARTIAL_CONNECTIVITY.eventId));
     }
+
+    @Test
+    public void testNotifyNoInternetAsDialogWhenHighPriority() throws Exception {
+        doReturn(true).when(mResources).getBoolean(
+                R.bool.config_notifyNoInternetAsDialogWhenHighPriority);
+
+        mManager.showNotification(TEST_NOTIF_ID, NETWORK_SWITCH, mWifiNai, mCellNai, null, false);
+        // Non-"no internet" notifications are not affected
+        verify(mNotificationManager).notify(eq(TEST_NOTIF_TAG), eq(NETWORK_SWITCH.eventId), any());
+
+        final Instrumentation instr = InstrumentationRegistry.getInstrumentation();
+        final Context ctx = instr.getContext();
+        final String testAction = "com.android.connectivity.coverage.TEST_DIALOG";
+        final Intent intent = new Intent(testAction)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .setClassName(ctx.getPackageName(), TestDialogActivity.class.getName());
+        final PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0 /* requestCode */,
+                intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+        mManager.showNotification(TEST_NOTIF_ID, NO_INTERNET, mWifiNai, null /* switchToNai */,
+                pendingIntent, true /* highPriority */);
+
+        // Previous notifications are still dismissed
+        verify(mNotificationManager).cancel(TEST_NOTIF_TAG, NETWORK_SWITCH.eventId);
+
+        // Verify that the activity is shown (the activity shows the action on screen)
+        final UiObject actionText = UiDevice.getInstance(instr).findObject(
+                new UiSelector().text(testAction));
+        assertTrue("Activity not shown", actionText.waitForExists(TEST_TIMEOUT_MS));
+
+        // Tapping the text should dismiss the dialog
+        actionText.click();
+        assertTrue("Activity not dismissed", actionText.waitUntilGone(TEST_TIMEOUT_MS));
+
+        // Verify no NO_INTERNET notification was posted
+        verify(mNotificationManager, never()).notify(any(), eq(NO_INTERNET.eventId), any());
+    }
+
+    private void doNotificationTextTest(NotificationType type, @StringRes int expectedTitleRes,
+            String expectedTitleArg, @StringRes int expectedContentRes) {
+        final String expectedTitle = "title " + expectedTitleArg;
+        final String expectedContent = "expected content";
+        doReturn(expectedTitle).when(mResources).getString(expectedTitleRes, expectedTitleArg);
+        doReturn(expectedContent).when(mResources).getString(expectedContentRes);
+
+        mManager.showNotification(TEST_NOTIF_ID, type, mWifiNai, mCellNai, null, false);
+        final ArgumentCaptor<Notification> notifCap = ArgumentCaptor.forClass(Notification.class);
+
+        verify(mNotificationManager).notify(eq(TEST_NOTIF_TAG), eq(type.eventId),
+                notifCap.capture());
+        final Notification notif = notifCap.getValue();
+
+        assertEquals(expectedTitle, notif.extras.getString(Notification.EXTRA_TITLE));
+        assertEquals(expectedContent, notif.extras.getString(Notification.EXTRA_TEXT));
+    }
+
+    @Test
+    public void testNotificationText_NoInternet() {
+        doNotificationTextTest(NO_INTERNET,
+                R.string.wifi_no_internet, TEST_EXTRA_INFO,
+                R.string.wifi_no_internet_detailed);
+    }
+
+    @Test
+    public void testNotificationText_Partial() {
+        doNotificationTextTest(PARTIAL_CONNECTIVITY,
+                R.string.network_partial_connectivity, TEST_EXTRA_INFO,
+                R.string.network_partial_connectivity_detailed);
+    }
+
+    @Test
+    public void testNotificationText_PartialAsNoInternet() {
+        doReturn(true).when(mResources).getBoolean(
+                R.bool.config_partialConnectivityNotifiedAsNoInternet);
+        doNotificationTextTest(PARTIAL_CONNECTIVITY,
+                R.string.wifi_no_internet, TEST_EXTRA_INFO,
+                R.string.wifi_no_internet_detailed);
+    }
 }