1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.networkstack 18 19 import android.app.Notification 20 import android.app.NotificationChannel 21 import android.app.NotificationManager 22 import android.app.NotificationManager.IMPORTANCE_DEFAULT 23 import android.app.NotificationManager.IMPORTANCE_NONE 24 import android.app.PendingIntent 25 import android.app.PendingIntent.FLAG_IMMUTABLE 26 import android.content.Context 27 import android.content.Intent 28 import android.content.res.Resources 29 import android.net.CaptivePortalData 30 import android.net.ConnectivityManager 31 import android.net.ConnectivityManager.EXTRA_NETWORK 32 import android.net.ConnectivityManager.NetworkCallback 33 import android.net.LinkProperties 34 import android.net.Network 35 import android.net.NetworkCapabilities 36 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED 37 import android.net.NetworkCapabilities.TRANSPORT_WIFI 38 import android.net.Uri 39 import android.os.Handler 40 import android.os.UserHandle 41 import android.provider.Settings 42 import android.testing.AndroidTestingRunner 43 import android.testing.TestableLooper 44 import android.testing.TestableLooper.RunWithLooper 45 import androidx.test.filters.SmallTest 46 import androidx.test.platform.app.InstrumentationRegistry 47 import com.android.dx.mockito.inline.extended.ExtendedMockito.verify 48 import com.android.networkstack.NetworkStackNotifier.CHANNEL_CONNECTED 49 import com.android.networkstack.NetworkStackNotifier.CHANNEL_VENUE_INFO 50 import com.android.networkstack.NetworkStackNotifier.CONNECTED_NOTIFICATION_TIMEOUT_MS 51 import com.android.networkstack.NetworkStackNotifier.Dependencies 52 import com.android.networkstack.apishim.NetworkInformationShimImpl 53 import com.android.modules.utils.build.SdkLevel.isAtLeastR 54 import com.android.modules.utils.build.SdkLevel.isAtLeastS 55 import org.junit.Assume.assumeTrue 56 import org.junit.Before 57 import org.junit.Test 58 import org.junit.runner.RunWith 59 import org.mockito.ArgumentCaptor 60 import org.mockito.ArgumentMatchers.anyInt 61 import org.mockito.ArgumentMatchers.eq 62 import org.mockito.ArgumentMatchers.intThat 63 import org.mockito.Captor 64 import org.mockito.Mock 65 import org.mockito.Mockito.any 66 import org.mockito.Mockito.doReturn 67 import org.mockito.Mockito.never 68 import org.mockito.MockitoAnnotations 69 import kotlin.reflect.KClass 70 import kotlin.test.assertEquals 71 72 @RunWith(AndroidTestingRunner::class) 73 @SmallTest 74 @RunWithLooper 75 class NetworkStackNotifierTest { 76 @Mock 77 private lateinit var mContext: Context 78 @Mock 79 private lateinit var mCurrentUserContext: Context 80 @Mock 81 private lateinit var mAllUserContext: Context 82 @Mock 83 private lateinit var mDependencies: Dependencies 84 @Mock 85 private lateinit var mNm: NotificationManager 86 @Mock 87 private lateinit var mNotificationChannelsNm: NotificationManager 88 @Mock 89 private lateinit var mCm: ConnectivityManager 90 @Mock 91 private lateinit var mResources: Resources 92 @Mock 93 private lateinit var mPendingIntent: PendingIntent 94 @Captor 95 private lateinit var mNoteCaptor: ArgumentCaptor<Notification> 96 @Captor 97 private lateinit var mNoteIdCaptor: ArgumentCaptor<Int> 98 @Captor 99 private lateinit var mIntentCaptor: ArgumentCaptor<Intent> 100 private lateinit var mLooper: TestableLooper 101 private lateinit var mHandler: Handler 102 private lateinit var mNotifier: NetworkStackNotifier 103 104 private lateinit var mAllNetworksCb: NetworkCallback 105 private lateinit var mDefaultNetworkCb: NetworkCallback 106 107 // Lazy-init as CaptivePortalData does not exist on Q. <lambda>null108 private val mTestCapportLp by lazy { 109 LinkProperties().apply { 110 captivePortalData = CaptivePortalData.Builder() 111 .setCaptive(false) 112 .setVenueInfoUrl(Uri.parse(TEST_VENUE_INFO_URL)) 113 .build() 114 } 115 } 116 <lambda>null117 private val mTestCapportVenueUrlWithFriendlyNameLp by lazy { 118 LinkProperties().apply { 119 captivePortalData = CaptivePortalData.Builder() 120 .setCaptive(false) 121 .setVenueInfoUrl(Uri.parse(TEST_VENUE_INFO_URL)) 122 .build() 123 val networkShim = NetworkInformationShimImpl.newInstance() 124 val captivePortalDataShim = networkShim.getCaptivePortalData(this) 125 126 if (captivePortalDataShim != null) { 127 networkShim.setCaptivePortalData(this, captivePortalDataShim 128 .withVenueFriendlyName(TEST_NETWORK_FRIENDLY_NAME)) 129 } 130 } 131 } 132 133 private val TEST_NETWORK = Network(42) 134 private val TEST_NETWORK_TAG = TEST_NETWORK.networkHandle.toString() 135 private val TEST_SSID = "TestSsid" 136 private val EMPTY_CAPABILITIES = NetworkCapabilities() 137 private val VALIDATED_CAPABILITIES = NetworkCapabilities() 138 .addTransportType(TRANSPORT_WIFI) 139 .addCapability(NET_CAPABILITY_VALIDATED) 140 141 private val TEST_CONNECTED_DESCRIPTION = "Connected" 142 private val TEST_VENUE_DESCRIPTION = "Connected / Tap to view website" 143 144 private val TEST_VENUE_INFO_URL = "https://testvenue.example.com/info" 145 private val EMPTY_CAPPORT_LP = LinkProperties() 146 private val TEST_NETWORK_FRIENDLY_NAME = "Network Friendly Name" 147 148 @Before setUpnull149 fun setUp() { 150 MockitoAnnotations.initMocks(this) 151 mLooper = TestableLooper.get(this) 152 doReturn(mResources).`when`(mContext).resources 153 doReturn(TEST_CONNECTED_DESCRIPTION).`when`(mResources).getString(R.string.connected) 154 doReturn(TEST_VENUE_DESCRIPTION).`when`(mResources).getString(R.string.tap_for_info) 155 156 // applicationInfo is used by Notification.Builder 157 val realContext = InstrumentationRegistry.getInstrumentation().context 158 doReturn(realContext.applicationInfo).`when`(mContext).applicationInfo 159 doReturn(realContext.packageName).`when`(mContext).packageName 160 161 doReturn(mCurrentUserContext).`when`(mContext).createPackageContextAsUser( 162 realContext.packageName, 0, UserHandle.CURRENT) 163 doReturn(mAllUserContext).`when`(mContext).createPackageContextAsUser( 164 realContext.packageName, 0, UserHandle.ALL) 165 166 mAllUserContext.mockService(Context.NOTIFICATION_SERVICE, NotificationManager::class, mNm) 167 mContext.mockService(Context.NOTIFICATION_SERVICE, NotificationManager::class, 168 mNotificationChannelsNm) 169 mContext.mockService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class, mCm) 170 171 doReturn(NotificationChannel(CHANNEL_VENUE_INFO, "TestChannel", IMPORTANCE_DEFAULT)) 172 .`when`(mNotificationChannelsNm).getNotificationChannel(CHANNEL_VENUE_INFO) 173 174 doReturn(mPendingIntent).`when`(mDependencies).getActivityPendingIntent( 175 any(), any(), anyInt()) 176 mNotifier = NetworkStackNotifier(mContext, mLooper.looper, mDependencies) 177 mHandler = mNotifier.handler 178 179 val allNetworksCbCaptor = ArgumentCaptor.forClass(NetworkCallback::class.java) 180 verify(mCm).registerNetworkCallback(any() /* request */, allNetworksCbCaptor.capture(), 181 eq(mHandler)) 182 mAllNetworksCb = allNetworksCbCaptor.value 183 184 val defaultNetworkCbCaptor = ArgumentCaptor.forClass(NetworkCallback::class.java) 185 verify(mCm).registerDefaultNetworkCallback(defaultNetworkCbCaptor.capture(), eq(mHandler)) 186 mDefaultNetworkCb = defaultNetworkCbCaptor.value 187 } 188 mockServicenull189 private fun <T : Any> Context.mockService(name: String, clazz: KClass<T>, service: T) { 190 doReturn(service).`when`(this).getSystemService(name) 191 doReturn(name).`when`(this).getSystemServiceName(clazz.java) 192 doReturn(service).`when`(this).getSystemService(clazz.java) 193 } 194 195 @Test testNoNotificationnull196 fun testNoNotification() { 197 onCapabilitiesChanged(EMPTY_CAPABILITIES) 198 onCapabilitiesChanged(VALIDATED_CAPABILITIES) 199 200 mLooper.processAllMessages() 201 verify(mNm, never()).notify(any(), anyInt(), any()) 202 } 203 verifyConnectedNotificationnull204 private fun verifyConnectedNotification(timeout: Long = CONNECTED_NOTIFICATION_TIMEOUT_MS) { 205 verify(mNm).notify(eq(TEST_NETWORK_TAG), mNoteIdCaptor.capture(), mNoteCaptor.capture()) 206 val note = mNoteCaptor.value 207 assertEquals(mPendingIntent, note.contentIntent) 208 assertEquals(CHANNEL_CONNECTED, note.channelId) 209 assertEquals(timeout, note.timeoutAfter) 210 verify(mDependencies).getActivityPendingIntent( 211 eq(mCurrentUserContext), mIntentCaptor.capture(), 212 intThat { it or FLAG_IMMUTABLE != 0 }) 213 } 214 verifyCanceledNotificationAfterNetworkLostnull215 private fun verifyCanceledNotificationAfterNetworkLost() { 216 onLost(TEST_NETWORK) 217 mLooper.processAllMessages() 218 verify(mNm).cancel(TEST_NETWORK_TAG, mNoteIdCaptor.value) 219 } 220 verifyCanceledNotificationAfterDefaultNetworkLostnull221 private fun verifyCanceledNotificationAfterDefaultNetworkLost() { 222 onDefaultNetworkLost(TEST_NETWORK) 223 mLooper.processAllMessages() 224 verify(mNm).cancel(TEST_NETWORK_TAG, mNoteIdCaptor.value) 225 } 226 227 @Test testConnectedNotification_NoSsidnull228 fun testConnectedNotification_NoSsid() { 229 onCapabilitiesChanged(EMPTY_CAPABILITIES) 230 mNotifier.notifyCaptivePortalValidationPending(TEST_NETWORK) 231 onCapabilitiesChanged(VALIDATED_CAPABILITIES) 232 mLooper.processAllMessages() 233 // There is no notification when SSID is not set. 234 verify(mNm, never()).notify(any(), anyInt(), any()) 235 } 236 237 @Test testConnectedNotification_WithSsidnull238 fun testConnectedNotification_WithSsid() { 239 // NetworkCapabilities#getSSID is not available for API <= Q 240 assumeTrue(isAtLeastR()) 241 val capabilities = NetworkCapabilities(VALIDATED_CAPABILITIES).setSSID(TEST_SSID) 242 243 onCapabilitiesChanged(EMPTY_CAPABILITIES) 244 mNotifier.notifyCaptivePortalValidationPending(TEST_NETWORK) 245 onCapabilitiesChanged(capabilities) 246 mLooper.processAllMessages() 247 248 verifyConnectedNotification() 249 verify(mResources).getString(R.string.connected) 250 verifyWifiSettingsIntent(mIntentCaptor.value) 251 verifyCanceledNotificationAfterNetworkLost() 252 } 253 254 @Test testConnectedVenueInfoNotificationnull255 fun testConnectedVenueInfoNotification() { 256 // Venue info (CaptivePortalData) is not available for API <= Q 257 assumeTrue(isAtLeastR()) 258 mNotifier.notifyCaptivePortalValidationPending(TEST_NETWORK) 259 onLinkPropertiesChanged(mTestCapportLp) 260 onDefaultNetworkAvailable(TEST_NETWORK) 261 val capabilities = NetworkCapabilities(VALIDATED_CAPABILITIES).setSSID(TEST_SSID) 262 onCapabilitiesChanged(capabilities) 263 264 mLooper.processAllMessages() 265 266 verifyConnectedNotification(timeout = 0) 267 verifyVenueInfoIntent(mIntentCaptor.value) 268 verify(mResources).getString(R.string.tap_for_info) 269 verifyCanceledNotificationAfterDefaultNetworkLost() 270 } 271 272 @Test testConnectedVenueInfoNotification_VenueInfoDisablednull273 fun testConnectedVenueInfoNotification_VenueInfoDisabled() { 274 // Venue info (CaptivePortalData) is not available for API <= Q 275 assumeTrue(isAtLeastR()) 276 val channel = NotificationChannel(CHANNEL_VENUE_INFO, "test channel", IMPORTANCE_NONE) 277 doReturn(channel).`when`(mNotificationChannelsNm).getNotificationChannel(CHANNEL_VENUE_INFO) 278 mNotifier.notifyCaptivePortalValidationPending(TEST_NETWORK) 279 onLinkPropertiesChanged(mTestCapportLp) 280 onDefaultNetworkAvailable(TEST_NETWORK) 281 val capabilities = NetworkCapabilities(VALIDATED_CAPABILITIES).setSSID(TEST_SSID) 282 onCapabilitiesChanged(capabilities) 283 mLooper.processAllMessages() 284 285 verifyConnectedNotification() 286 verifyWifiSettingsIntent(mIntentCaptor.value) 287 verify(mResources, never()).getString(R.string.tap_for_info) 288 verifyCanceledNotificationAfterNetworkLost() 289 } 290 291 @Test testVenueInfoNotificationnull292 fun testVenueInfoNotification() { 293 // Venue info (CaptivePortalData) is not available for API <= Q 294 assumeTrue(isAtLeastR()) 295 onLinkPropertiesChanged(mTestCapportLp) 296 onDefaultNetworkAvailable(TEST_NETWORK) 297 val capabilities = NetworkCapabilities(VALIDATED_CAPABILITIES).setSSID(TEST_SSID) 298 onCapabilitiesChanged(capabilities) 299 mLooper.processAllMessages() 300 301 verify(mNm).notify(eq(TEST_NETWORK_TAG), mNoteIdCaptor.capture(), mNoteCaptor.capture()) 302 verify(mDependencies).getActivityPendingIntent( 303 eq(mCurrentUserContext), mIntentCaptor.capture(), 304 intThat { it or FLAG_IMMUTABLE != 0 }) 305 verifyVenueInfoIntent(mIntentCaptor.value) 306 verifyCanceledNotificationAfterDefaultNetworkLost() 307 } 308 309 @Test testVenueInfoNotification_VenueInfoDisablednull310 fun testVenueInfoNotification_VenueInfoDisabled() { 311 // Venue info (CaptivePortalData) is not available for API <= Q 312 assumeTrue(isAtLeastR()) 313 doReturn(null).`when`(mNm).getNotificationChannel(CHANNEL_VENUE_INFO) 314 onLinkPropertiesChanged(mTestCapportLp) 315 onDefaultNetworkAvailable(TEST_NETWORK) 316 onCapabilitiesChanged(VALIDATED_CAPABILITIES) 317 mLooper.processAllMessages() 318 319 verify(mNm, never()).notify(any(), anyInt(), any()) 320 } 321 322 @Test testNonDefaultVenueInfoNotificationnull323 fun testNonDefaultVenueInfoNotification() { 324 // Venue info (CaptivePortalData) is not available for API <= Q 325 assumeTrue(isAtLeastR()) 326 onLinkPropertiesChanged(mTestCapportLp) 327 onCapabilitiesChanged(VALIDATED_CAPABILITIES) 328 mLooper.processAllMessages() 329 330 verify(mNm, never()).notify(eq(TEST_NETWORK_TAG), anyInt(), any()) 331 } 332 333 @Test testEmptyCaptivePortalDataVenueInfoNotificationnull334 fun testEmptyCaptivePortalDataVenueInfoNotification() { 335 // Venue info (CaptivePortalData) is not available for API <= Q 336 assumeTrue(isAtLeastR()) 337 onLinkPropertiesChanged(EMPTY_CAPPORT_LP) 338 onCapabilitiesChanged(VALIDATED_CAPABILITIES) 339 mLooper.processAllMessages() 340 341 verify(mNm, never()).notify(eq(TEST_NETWORK_TAG), anyInt(), any()) 342 } 343 344 @Test testUnvalidatedNetworkVenueInfoNotificationnull345 fun testUnvalidatedNetworkVenueInfoNotification() { 346 // Venue info (CaptivePortalData) is not available for API <= Q 347 assumeTrue(isAtLeastR()) 348 onLinkPropertiesChanged(mTestCapportLp) 349 onCapabilitiesChanged(EMPTY_CAPABILITIES) 350 mLooper.processAllMessages() 351 352 verify(mNm, never()).notify(eq(TEST_NETWORK_TAG), anyInt(), any()) 353 } 354 355 @Test testConnectedVenueInfoWithFriendlyNameNotificationnull356 fun testConnectedVenueInfoWithFriendlyNameNotification() { 357 // Venue info (CaptivePortalData) with friendly name is not available for API <= R 358 assumeTrue(isAtLeastS()) 359 mNotifier.notifyCaptivePortalValidationPending(TEST_NETWORK) 360 onLinkPropertiesChanged(mTestCapportVenueUrlWithFriendlyNameLp) 361 onDefaultNetworkAvailable(TEST_NETWORK) 362 val capabilities = NetworkCapabilities(VALIDATED_CAPABILITIES).setSSID(TEST_SSID) 363 onCapabilitiesChanged(capabilities) 364 365 mLooper.processAllMessages() 366 367 verifyConnectedNotification(timeout = 0) 368 verifyVenueInfoIntent(mIntentCaptor.value) 369 verify(mResources).getString(R.string.tap_for_info) 370 verify(mNm).notify(eq(TEST_NETWORK_TAG), mNoteIdCaptor.capture(), mNoteCaptor.capture()) 371 val note = mNoteCaptor.value 372 assertEquals(TEST_NETWORK_FRIENDLY_NAME, note.extras 373 .getCharSequence(Notification.EXTRA_TITLE)) 374 verifyCanceledNotificationAfterDefaultNetworkLost() 375 } 376 verifyVenueInfoIntentnull377 private fun verifyVenueInfoIntent(intent: Intent) { 378 assertEquals(Intent.ACTION_VIEW, intent.action) 379 assertEquals(Uri.parse(TEST_VENUE_INFO_URL), intent.data) 380 assertEquals<Network?>(TEST_NETWORK, intent.getParcelableExtra(EXTRA_NETWORK)) 381 } 382 verifyWifiSettingsIntentnull383 private fun verifyWifiSettingsIntent(intent: Intent) { 384 assertEquals(Settings.ACTION_WIFI_SETTINGS, intent.action) 385 } 386 onDefaultNetworkAvailablenull387 private fun onDefaultNetworkAvailable(network: Network) { 388 mHandler.post { 389 mDefaultNetworkCb.onAvailable(network) 390 } 391 } 392 onDefaultNetworkLostnull393 private fun onDefaultNetworkLost(network: Network) { 394 mHandler.post { 395 mDefaultNetworkCb.onLost(network) 396 } 397 } 398 onCapabilitiesChangednull399 private fun onCapabilitiesChanged(capabilities: NetworkCapabilities) { 400 mHandler.post { 401 mAllNetworksCb.onCapabilitiesChanged(TEST_NETWORK, capabilities) 402 } 403 } 404 onLinkPropertiesChangednull405 private fun onLinkPropertiesChanged(lp: LinkProperties) { 406 mHandler.post { 407 mAllNetworksCb.onLinkPropertiesChanged(TEST_NETWORK, lp) 408 } 409 } 410 onLostnull411 private fun onLost(network: Network) { 412 mHandler.post { 413 mAllNetworksCb.onLost(network) 414 } 415 } 416 }