diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml index bf32ad5ac5..b22457a9bb 100644 --- a/service/ServiceConnectivityResources/res/values/config.xml +++ b/service/ServiceConnectivityResources/res/values/config.xml @@ -114,4 +114,15 @@ true + + false + + + false + diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml index 6ac6a0e6a6..5af13d764b 100644 --- a/service/ServiceConnectivityResources/res/values/overlayable.xml +++ b/service/ServiceConnectivityResources/res/values/overlayable.xml @@ -32,6 +32,8 @@ + + diff --git a/service/src/com/android/server/connectivity/NetworkNotificationManager.java b/service/src/com/android/server/connectivity/NetworkNotificationManager.java index ae98d92073..155f6c4395 100644 --- a/service/src/com/android/server/connectivity/NetworkNotificationManager.java +++ b/service/src/com/android/server/connectivity/NetworkNotificationManager.java @@ -198,11 +198,22 @@ public class NetworkNotificationManager { } 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 @@ public class NetworkNotificationManager { } } + 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/integration/Android.bp b/tests/integration/Android.bp index 26f7f4aaee..7b5b44f731 100644 --- a/tests/integration/Android.bp +++ b/tests/integration/Android.bp @@ -30,6 +30,7 @@ android_test { ], 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 e039ef0725..80338aa2c0 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_AUTO_CREATE 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.NetworkRequest 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.testing.TestableContext 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.ArgumentMatchers.anyString 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 @@ class ConnectivityServiceIntegrationTest { 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 @@ class ConnectivityServiceIntegrationTest { 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 @@ class ConnectivityServiceIntegrationTest { .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("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()).`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()).`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 @@ class ConnectivityServiceIntegrationTest { 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 c8476cb726..71bd6089cc 100644 --- a/tests/unit/Android.bp +++ b/tests/unit/Android.bp @@ -62,6 +62,7 @@ android_library { 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 4c60ccf606..887f17158b 100644 --- a/tests/unit/AndroidManifest.xml +++ b/tests/unit/AndroidManifest.xml @@ -53,6 +53,8 @@ + finish()); + setContentView(txt); + } + } + @Mock Context mCtx; @Mock Resources mResources; @Mock DisplayMetrics mDisplayMetrics; @@ -345,4 +379,82 @@ public class NetworkNotificationManagerTest { 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 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); + } }