Merge changes Ic33d8976,Ib5bd74d8 into sc-dev

* changes:
  Mock connectivity resources in integration tests
  Add overlay options for no internet notifications
This commit is contained in:
Remi NGUYEN VAN
2021-08-05 07:17:31 +00:00
committed by Android (Google) Code Review
8 changed files with 200 additions and 3 deletions

View File

@@ -114,4 +114,15 @@
<!-- Whether to cancel network notifications automatically when tapped --> <!-- Whether to cancel network notifications automatically when tapped -->
<bool name="config_autoCancelNetworkNotifications">true</bool> <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> </resources>

View File

@@ -32,6 +32,8 @@
<item type="array" name="config_networkNotifySwitches"/> <item type="array" name="config_networkNotifySwitches"/>
<item type="bool" name="config_ongoingSignInNotification"/> <item type="bool" name="config_ongoingSignInNotification"/>
<item type="bool" name="config_autoCancelNetworkNotifications"/> <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_wifi_in_range"/>
<item type="drawable" name="stat_notify_rssi_in_range"/> <item type="drawable" name="stat_notify_rssi_in_range"/>
</policy> </policy>

View File

@@ -198,11 +198,22 @@ public class NetworkNotificationManager {
} }
final Resources r = mResources.get(); 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 title;
final CharSequence details; final CharSequence details;
Icon icon = Icon.createWithResource( Icon icon = Icon.createWithResource(
mResources.getResourcesContext(), getIcon(transportType)); 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); title = r.getString(R.string.wifi_no_internet, name);
details = r.getString(R.string.wifi_no_internet_detailed); details = r.getString(R.string.wifi_no_internet_detailed);
} else if (notifyType == NotificationType.PRIVATE_DNS_BROKEN) { } 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. * Clear the notification with the given id, only if it matches the given type.
*/ */

View File

@@ -30,6 +30,7 @@ android_test {
], ],
libs: [ libs: [
"android.test.mock", "android.test.mock",
"ServiceConnectivityResources",
], ],
static_libs: [ static_libs: [
"NetworkStackApiStableLib", "NetworkStackApiStableLib",

View File

@@ -23,7 +23,9 @@ import android.content.Context.BIND_AUTO_CREATE
import android.content.Context.BIND_IMPORTANT import android.content.Context.BIND_IMPORTANT
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.res.Resources
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.ConnectivityResources
import android.net.IDnsResolver import android.net.IDnsResolver
import android.net.INetd import android.net.INetd
import android.net.LinkProperties import android.net.LinkProperties
@@ -35,6 +37,7 @@ import android.net.NetworkRequest
import android.net.TestNetworkStackClient import android.net.TestNetworkStackClient
import android.net.Uri import android.net.Uri
import android.net.metrics.IpConnectivityLog import android.net.metrics.IpConnectivityLog
import android.net.util.MultinetworkPolicyTracker
import android.os.ConditionVariable import android.os.ConditionVariable
import android.os.IBinder import android.os.IBinder
import android.os.SystemConfigManager import android.os.SystemConfigManager
@@ -43,6 +46,7 @@ import android.testing.TestableContext
import android.util.Log import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.android.connectivity.resources.R
import com.android.server.ConnectivityService import com.android.server.ConnectivityService
import com.android.server.NetworkAgentWrapper import com.android.server.NetworkAgentWrapper
import com.android.server.TestNetIdManager import com.android.server.TestNetIdManager
@@ -59,6 +63,7 @@ import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.any import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doNothing import org.mockito.Mockito.doNothing
import org.mockito.Mockito.doReturn import org.mockito.Mockito.doReturn
import org.mockito.Mockito.eq import org.mockito.Mockito.eq
@@ -93,6 +98,10 @@ class ConnectivityServiceIntegrationTest {
private lateinit var dnsResolver: IDnsResolver private lateinit var dnsResolver: IDnsResolver
@Mock @Mock
private lateinit var systemConfigManager: SystemConfigManager private lateinit var systemConfigManager: SystemConfigManager
@Mock
private lateinit var resources: Resources
@Mock
private lateinit var resourcesContext: Context
@Spy @Spy
private var context = TestableContext(realContext) private var context = TestableContext(realContext)
@@ -110,9 +119,11 @@ class ConnectivityServiceIntegrationTest {
private val realContext get() = InstrumentationRegistry.getInstrumentation().context private val realContext get() = InstrumentationRegistry.getInstrumentation().context
private val httpProbeUrl get() = 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() = 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 { private class InstrumentationServiceConnection : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
@@ -156,6 +167,27 @@ class ConnectivityServiceIntegrationTest {
.getSystemService(Context.SYSTEM_CONFIG_SERVICE) .getSystemService(Context.SYSTEM_CONFIG_SERVICE)
doReturn(IntArray(0)).`when`(systemConfigManager).getSystemPermissionUids(anyString()) 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 = TestNetworkStackClient(realContext)
networkStackClient.start() networkStackClient.start()
@@ -176,12 +208,19 @@ class ConnectivityServiceIntegrationTest {
doReturn(mock(ProxyTracker::class.java)).`when`(deps).makeProxyTracker(any(), any()) doReturn(mock(ProxyTracker::class.java)).`when`(deps).makeProxyTracker(any(), any())
doReturn(mock(MockableSystemProperties::class.java)).`when`(deps).systemProperties doReturn(mock(MockableSystemProperties::class.java)).`when`(deps).systemProperties
doReturn(TestNetIdManager()).`when`(deps).makeNetIdManager() 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 return deps
} }
@After @After
fun tearDown() { fun tearDown() {
nsInstrumentation.clearAllState() nsInstrumentation.clearAllState()
ConnectivityResources.setResourcesContextForTest(null)
} }
@Test @Test

View File

@@ -62,6 +62,7 @@ android_library {
jarjar_rules: "jarjar-rules.txt", jarjar_rules: "jarjar-rules.txt",
static_libs: [ static_libs: [
"androidx.test.rules", "androidx.test.rules",
"androidx.test.uiautomator",
"bouncycastle-repackaged-unbundled", "bouncycastle-repackaged-unbundled",
"core-tests-support", "core-tests-support",
"FrameworksNetCommonTests", "FrameworksNetCommonTests",

View File

@@ -53,6 +53,8 @@
<application> <application>
<uses-library android:name="android.test.runner" /> <uses-library android:name="android.test.runner" />
<uses-library android:name="android.net.ipsec.ike" /> <uses-library android:name="android.net.ipsec.ike" />
<activity
android:name="com.android.server.connectivity.NetworkNotificationManagerTest$TestDialogActivity"/>
</application> </application>
<instrumentation <instrumentation

View File

@@ -27,6 +27,7 @@ import static com.android.server.connectivity.NetworkNotificationManager.Notific
import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.SIGN_IN; import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.SIGN_IN;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.clearInvocations;
@@ -39,9 +40,14 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; 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.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.Resources; import android.content.res.Resources;
@@ -49,11 +55,19 @@ import android.net.ConnectivityResources;
import android.net.NetworkCapabilities; import android.net.NetworkCapabilities;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.os.Build; import android.os.Build;
import android.os.Bundle;
import android.os.UserHandle; import android.os.UserHandle;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.test.filters.SmallTest; 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.connectivity.resources.R;
import com.android.server.connectivity.NetworkNotificationManager.NotificationType; import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
@@ -84,6 +98,7 @@ public class NetworkNotificationManagerTest {
private static final String TEST_EXTRA_INFO = "extra"; private static final String TEST_EXTRA_INFO = "extra";
private static final int TEST_NOTIF_ID = 101; private static final int TEST_NOTIF_ID = 101;
private static final String TEST_NOTIF_TAG = NetworkNotificationManager.tagFor(TEST_NOTIF_ID); 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 CELL_CAPABILITIES = new NetworkCapabilities();
static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities(); static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities();
static final NetworkCapabilities VPN_CAPABILITIES = new NetworkCapabilities(); static final NetworkCapabilities VPN_CAPABILITIES = new NetworkCapabilities();
@@ -102,6 +117,25 @@ public class NetworkNotificationManagerTest {
VPN_CAPABILITIES.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN); 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 Context mCtx;
@Mock Resources mResources; @Mock Resources mResources;
@Mock DisplayMetrics mDisplayMetrics; @Mock DisplayMetrics mDisplayMetrics;
@@ -345,4 +379,82 @@ public class NetworkNotificationManagerTest {
mManager.clearNotification(id, PARTIAL_CONNECTIVITY); mManager.clearNotification(id, PARTIAL_CONNECTIVITY);
verify(mNotificationManager, never()).cancel(eq(tag), eq(PARTIAL_CONNECTIVITY.eventId)); 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);
}
} }