blob: e038c4433ceb07d1792602967ac880be0d5e730b [file] [log] [blame]
/*
* Copyright (C) 2016 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.connectivity;
import static android.app.Notification.FLAG_AUTO_CANCEL;
import static android.app.Notification.FLAG_ONGOING_EVENT;
import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.LOST_INTERNET;
import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.NETWORK_SWITCH;
import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.NO_INTERNET;
import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.PARTIAL_CONNECTIVITY;
import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.PRIVATE_DNS_BROKEN;
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;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
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;
import android.net.ConnectivityResources;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.UserHandle;
import android.telephony.TelephonyManager;
import android.testing.PollingCheck;
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.By;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiSelector;
import androidx.test.uiautomator.Until;
import com.android.connectivity.resources.R;
import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.AdditionalAnswers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
public class NetworkNotificationManagerTest {
private static final String TEST_SSID = "Test SSID";
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;
private static final long UI_AUTOMATOR_WAIT_TIME_MILLIS = TEST_TIMEOUT_MS;
static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities();
static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities();
static final NetworkCapabilities VPN_CAPABILITIES = new NetworkCapabilities();
static {
CELL_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
CELL_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
WIFI_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
WIFI_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
WIFI_CAPABILITIES.setSSID(TEST_SSID);
// Set the underyling network to wifi.
VPN_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
VPN_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_VPN);
VPN_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
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;
@Mock PackageManager mPm;
@Mock TelephonyManager mTelephonyManager;
@Mock NotificationManager mNotificationManager;
@Mock NetworkAgentInfo mWifiNai;
@Mock NetworkAgentInfo mCellNai;
@Mock NetworkAgentInfo mVpnNai;
@Mock NetworkInfo mNetworkInfo;
ArgumentCaptor<Notification> mCaptor;
NetworkNotificationManager mManager;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mCaptor = ArgumentCaptor.forClass(Notification.class);
mWifiNai.networkCapabilities = WIFI_CAPABILITIES;
mWifiNai.networkInfo = mNetworkInfo;
mCellNai.networkCapabilities = CELL_CAPABILITIES;
mCellNai.networkInfo = mNetworkInfo;
mVpnNai.networkCapabilities = VPN_CAPABILITIES;
mVpnNai.networkInfo = mNetworkInfo;
mDisplayMetrics.density = 2.275f;
doReturn(true).when(mVpnNai).isVPN();
when(mCtx.getResources()).thenReturn(mResources);
when(mCtx.getPackageManager()).thenReturn(mPm);
when(mCtx.getApplicationInfo()).thenReturn(new ApplicationInfo());
final Context asUserCtx = mock(Context.class, AdditionalAnswers.delegatesTo(mCtx));
doReturn(UserHandle.ALL).when(asUserCtx).getUser();
when(mCtx.createContextAsUser(eq(UserHandle.ALL), anyInt())).thenReturn(asUserCtx);
when(mCtx.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
.thenReturn(mNotificationManager);
when(mNetworkInfo.getExtraInfo()).thenReturn(TEST_EXTRA_INFO);
ConnectivityResources.setResourcesContextForTest(mCtx);
when(mResources.getColor(anyInt(), any())).thenReturn(0xFF607D8B);
when(mResources.getDisplayMetrics()).thenReturn(mDisplayMetrics);
// Come up with some credible-looking transport names. The actual values do not matter.
String[] transportNames = new String[NetworkCapabilities.MAX_TRANSPORT + 1];
for (int transport = 0; transport <= NetworkCapabilities.MAX_TRANSPORT; transport++) {
transportNames[transport] = NetworkCapabilities.transportNameOf(transport);
}
when(mResources.getStringArray(R.array.network_switch_type_name))
.thenReturn(transportNames);
when(mResources.getBoolean(R.bool.config_autoCancelNetworkNotifications)).thenReturn(true);
mManager = new NetworkNotificationManager(mCtx, mTelephonyManager);
}
@After
public void tearDown() {
ConnectivityResources.setResourcesContextForTest(null);
}
private void verifyTitleByNetwork(final int id, final NetworkAgentInfo nai, final int title) {
final String tag = NetworkNotificationManager.tagFor(id);
mManager.showNotification(id, PRIVATE_DNS_BROKEN, nai, null, null, true);
verify(mNotificationManager, times(1))
.notify(eq(tag), eq(PRIVATE_DNS_BROKEN.eventId), any());
final int transportType = NetworkNotificationManager.approximateTransportType(nai);
if (transportType == NetworkCapabilities.TRANSPORT_WIFI) {
verify(mResources, times(1)).getString(eq(title), eq(TEST_EXTRA_INFO));
} else {
verify(mResources, times(1)).getString(title);
}
verify(mResources, times(1)).getString(eq(R.string.private_dns_broken_detailed));
}
@Test
public void testTitleOfPrivateDnsBroken() {
// Test the title of mobile data.
verifyTitleByNetwork(100, mCellNai, R.string.mobile_no_internet);
clearInvocations(mResources);
// Test the title of wifi.
verifyTitleByNetwork(101, mWifiNai, R.string.wifi_no_internet);
clearInvocations(mResources);
// Test the title of other networks.
verifyTitleByNetwork(102, mVpnNai, R.string.other_networks_no_internet);
clearInvocations(mResources);
}
@Test
public void testNotificationsShownAndCleared() {
final int NETWORK_ID_BASE = 100;
List<NotificationType> types = Arrays.asList(NotificationType.values());
List<Integer> ids = new ArrayList<>(types.size());
for (int i = 0; i < types.size(); i++) {
ids.add(NETWORK_ID_BASE + i);
}
Collections.shuffle(ids);
Collections.shuffle(types);
for (int i = 0; i < ids.size(); i++) {
mManager.showNotification(ids.get(i), types.get(i), mWifiNai, mCellNai, null, false);
}
List<Integer> idsToClear = new ArrayList<>(ids);
Collections.shuffle(idsToClear);
for (int i = 0; i < ids.size(); i++) {
mManager.clearNotification(idsToClear.get(i));
}
for (int i = 0; i < ids.size(); i++) {
final int id = ids.get(i);
final int eventId = types.get(i).eventId;
final String tag = NetworkNotificationManager.tagFor(id);
verify(mNotificationManager, times(1)).notify(eq(tag), eq(eventId), any());
verify(mNotificationManager, times(1)).cancel(eq(tag), eq(eventId));
}
}
@Test
@Ignore
// Ignored because the code under test calls Log.wtf, which crashes the tests on eng builds.
// TODO: re-enable after fixing this (e.g., turn Log.wtf into exceptions that this test catches)
public void testNoInternetNotificationsNotShownForCellular() {
mManager.showNotification(100, NO_INTERNET, mCellNai, mWifiNai, null, false);
mManager.showNotification(101, LOST_INTERNET, mCellNai, mWifiNai, null, false);
verify(mNotificationManager, never()).notify(any(), anyInt(), any());
mManager.showNotification(102, NO_INTERNET, mWifiNai, mCellNai, null, false);
final int eventId = NO_INTERNET.eventId;
final String tag = NetworkNotificationManager.tagFor(102);
verify(mNotificationManager, times(1)).notify(eq(tag), eq(eventId), any());
}
@Test
public void testNotificationsNotShownIfNoInternetCapability() {
mWifiNai.networkCapabilities = new NetworkCapabilities();
mWifiNai.networkCapabilities .addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
mManager.showNotification(102, NO_INTERNET, mWifiNai, mCellNai, null, false);
mManager.showNotification(103, LOST_INTERNET, mWifiNai, mCellNai, null, false);
mManager.showNotification(104, NETWORK_SWITCH, mWifiNai, mCellNai, null, false);
verify(mNotificationManager, never()).notify(any(), anyInt(), any());
}
private void assertNotification(NotificationType type, boolean ongoing, boolean autoCancel) {
final ArgumentCaptor<Notification> noteCaptor = ArgumentCaptor.forClass(Notification.class);
mManager.showNotification(TEST_NOTIF_ID, type, mWifiNai, mCellNai, null, false);
verify(mNotificationManager, times(1)).notify(eq(TEST_NOTIF_TAG), eq(type.eventId),
noteCaptor.capture());
assertEquals("Notification ongoing flag should be " + (ongoing ? "set" : "unset"),
ongoing, (noteCaptor.getValue().flags & FLAG_ONGOING_EVENT) != 0);
assertEquals("Notification autocancel flag should be " + (autoCancel ? "set" : "unset"),
autoCancel, (noteCaptor.getValue().flags & FLAG_AUTO_CANCEL) != 0);
}
@Test
public void testDuplicatedNotificationsNoInternetThenSignIn() {
// Show first NO_INTERNET
assertNotification(NO_INTERNET, false /* ongoing */, true /* autoCancel */);
// Captive portal detection triggers SIGN_IN a bit later, clearing the previous NO_INTERNET
assertNotification(SIGN_IN, false /* ongoing */, true /* autoCancel */);
verify(mNotificationManager, times(1)).cancel(eq(TEST_NOTIF_TAG), eq(NO_INTERNET.eventId));
// Network disconnects
mManager.clearNotification(TEST_NOTIF_ID);
verify(mNotificationManager, times(1)).cancel(eq(TEST_NOTIF_TAG), eq(SIGN_IN.eventId));
}
@Test
public void testOngoingSignInNotification() {
doReturn(true).when(mResources).getBoolean(R.bool.config_ongoingSignInNotification);
// Show first NO_INTERNET
assertNotification(NO_INTERNET, false /* ongoing */, true /* autoCancel */);
// Captive portal detection triggers SIGN_IN a bit later, clearing the previous NO_INTERNET
assertNotification(SIGN_IN, true /* ongoing */, true /* autoCancel */);
verify(mNotificationManager, times(1)).cancel(eq(TEST_NOTIF_TAG), eq(NO_INTERNET.eventId));
// Network disconnects
mManager.clearNotification(TEST_NOTIF_ID);
verify(mNotificationManager, times(1)).cancel(eq(TEST_NOTIF_TAG), eq(SIGN_IN.eventId));
}
@Test
public void testNoAutoCancelNotification() {
doReturn(false).when(mResources).getBoolean(R.bool.config_autoCancelNetworkNotifications);
// Show NO_INTERNET, then SIGN_IN
assertNotification(NO_INTERNET, false /* ongoing */, false /* autoCancel */);
assertNotification(SIGN_IN, false /* ongoing */, false /* autoCancel */);
verify(mNotificationManager, times(1)).cancel(eq(TEST_NOTIF_TAG), eq(NO_INTERNET.eventId));
mManager.clearNotification(TEST_NOTIF_ID);
verify(mNotificationManager, times(1)).cancel(eq(TEST_NOTIF_TAG), eq(SIGN_IN.eventId));
}
@Test
public void testDuplicatedNotificationsSignInThenNoInternet() {
final int id = TEST_NOTIF_ID;
final String tag = TEST_NOTIF_TAG;
// Show first SIGN_IN
mManager.showNotification(id, SIGN_IN, mWifiNai, mCellNai, null, false);
verify(mNotificationManager, times(1)).notify(eq(tag), eq(SIGN_IN.eventId), any());
reset(mNotificationManager);
// NO_INTERNET arrives after, but is ignored.
mManager.showNotification(id, NO_INTERNET, mWifiNai, mCellNai, null, false);
verify(mNotificationManager, never()).cancel(any(), anyInt());
verify(mNotificationManager, never()).notify(any(), anyInt(), any());
// Network disconnects
mManager.clearNotification(id);
verify(mNotificationManager, times(1)).cancel(eq(tag), eq(SIGN_IN.eventId));
}
@Test
public void testClearNotificationByType() {
final int id = TEST_NOTIF_ID;
final String tag = TEST_NOTIF_TAG;
// clearNotification(int id, NotificationType notifyType) will check if given type is equal
// to previous type or not. If they are equal then clear the notification; if they are not
// equal then return.
mManager.showNotification(id, NO_INTERNET, mWifiNai, mCellNai, null, false);
verify(mNotificationManager, times(1)).notify(eq(tag), eq(NO_INTERNET.eventId), any());
// Previous notification is NO_INTERNET and given type is NO_INTERNET too. The notification
// should be cleared.
mManager.clearNotification(id, NO_INTERNET);
verify(mNotificationManager, times(1)).cancel(eq(tag), eq(NO_INTERNET.eventId));
// SIGN_IN is popped-up.
mManager.showNotification(id, SIGN_IN, mWifiNai, mCellNai, null, false);
verify(mNotificationManager, times(1)).notify(eq(tag), eq(SIGN_IN.eventId), any());
// The notification type is not matching previous one, PARTIAL_CONNECTIVITY won't be
// cleared.
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);
final Instrumentation instr = InstrumentationRegistry.getInstrumentation();
final UiDevice uiDevice = UiDevice.getInstance(instr);
final Context ctx = instr.getContext();
final PowerManager pm = ctx.getSystemService(PowerManager.class);
// Wake up the device (it has no effect if the device is already awake).
uiDevice.executeShellCommand("input keyevent KEYCODE_WAKEUP");
uiDevice.executeShellCommand("wm dismiss-keyguard");
PollingCheck.check("Wait for the screen to be turned on failed, timeout=" + TEST_TIMEOUT_MS,
TEST_TIMEOUT_MS, () -> pm.isInteractive());
uiDevice.pressHome();
// UiDevice.getLauncherPackageName() requires the test manifest to have a <queries> tag for
// the launcher intent.
final String launcherPackageName = uiDevice.getLauncherPackageName();
assertTrue(String.format("Launcher (%s) is not shown", launcherPackageName),
uiDevice.wait(Until.hasObject(By.pkg(launcherPackageName)),
UI_AUTOMATOR_WAIT_TIME_MILLIS));
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 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.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);
}
}