[TNU06] Add roaming notification

Warn user of potential data charges if the backhaul is
cellular and user is roaming.

Bug: 145629001
Test: atest TetheringTests
Change-Id: I74b4f87c2f6aad09e05d3f2a779f880396885953
diff --git a/Tethering/res/values-mcc310-mnc004/config.xml b/Tethering/res/values-mcc310-mnc004/config.xml
index 8c627d5..5c5be04 100644
--- a/Tethering/res/values-mcc310-mnc004/config.xml
+++ b/Tethering/res/values-mcc310-mnc004/config.xml
@@ -17,4 +17,7 @@
     <!-- Delay(millisecond) to show no upstream notification after there's no Backhaul. Set delay to
          "0" for disable this feature. -->
     <integer name="delay_to_show_no_upstream_after_no_backhaul">5000</integer>
+
+    <!-- Config for showing upstream roaming notification. -->
+    <bool name="config_upstream_roaming_notification">true</bool>
 </resources>
\ No newline at end of file
diff --git a/Tethering/res/values-mcc311-mnc480/config.xml b/Tethering/res/values-mcc311-mnc480/config.xml
index 8c627d5..5c5be04 100644
--- a/Tethering/res/values-mcc311-mnc480/config.xml
+++ b/Tethering/res/values-mcc311-mnc480/config.xml
@@ -17,4 +17,7 @@
     <!-- Delay(millisecond) to show no upstream notification after there's no Backhaul. Set delay to
          "0" for disable this feature. -->
     <integer name="delay_to_show_no_upstream_after_no_backhaul">5000</integer>
+
+    <!-- Config for showing upstream roaming notification. -->
+    <bool name="config_upstream_roaming_notification">true</bool>
 </resources>
\ No newline at end of file
diff --git a/Tethering/res/values/config.xml b/Tethering/res/values/config.xml
index 780a015..aed5ab8 100644
--- a/Tethering/res/values/config.xml
+++ b/Tethering/res/values/config.xml
@@ -206,4 +206,9 @@
     <!-- Delay(millisecond) to show no upstream notification after there's no Backhaul. Set delay to
          "-1" for disable this feature. -->
     <integer name="delay_to_show_no_upstream_after_no_backhaul">-1</integer>
+
+    <!-- Cellular roaming notification is shown when upstream is cellular network and in roaming
+         state. -->
+    <!-- Config for showing upstream roaming notification. -->
+    <bool name="config_upstream_roaming_notification">false</bool>
 </resources>
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 14d2886..97b1946 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -81,6 +81,7 @@
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.Network;
+import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
 import android.net.TetherStatesParcel;
 import android.net.TetheredClient;
@@ -1476,7 +1477,7 @@
             if (mTetherUpstream != newUpstream) {
                 mTetherUpstream = newUpstream;
                 mUpstreamNetworkMonitor.setCurrentUpstream(mTetherUpstream);
-                reportUpstreamChanged(mTetherUpstream);
+                reportUpstreamChanged(ns);
             }
         }
 
@@ -1598,7 +1599,8 @@
             }
         }
 
-        private void handleUpstreamNetworkMonitorCallback(int arg1, Object o) {
+        @VisibleForTesting
+        void handleUpstreamNetworkMonitorCallback(int arg1, Object o) {
             if (arg1 == UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES) {
                 mOffload.sendOffloadExemptPrefixes((Set<IpPrefix>) o);
                 return;
@@ -1624,6 +1626,9 @@
 
             switch (arg1) {
                 case UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES:
+                    if (ns.network.equals(mTetherUpstream)) {
+                        mNotificationUpdater.onUpstreamCapabilitiesChanged(ns.networkCapabilities);
+                    }
                     handleNewUpstreamNetworkState(ns);
                     break;
                 case UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES:
@@ -2009,8 +2014,10 @@
         });
     }
 
-    private void reportUpstreamChanged(Network network) {
+    private void reportUpstreamChanged(UpstreamNetworkState ns) {
         final int length = mTetheringEventCallbacks.beginBroadcast();
+        final Network network = (ns != null) ? ns.network : null;
+        final NetworkCapabilities capabilities = (ns != null) ? ns.networkCapabilities : null;
         try {
             for (int i = 0; i < length; i++) {
                 try {
@@ -2022,7 +2029,9 @@
         } finally {
             mTetheringEventCallbacks.finishBroadcast();
         }
-        mNotificationUpdater.onUpstreamNetworkChanged(network);
+        // Need to notify capabilities change after upstream network changed because new network's
+        // capabilities should be checked every time.
+        mNotificationUpdater.onUpstreamCapabilitiesChanged(capabilities);
     }
 
     private void reportConfigurationChanged(TetheringConfigurationParcel config) {
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java b/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
index de2f90e..f490cc4 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
@@ -16,6 +16,7 @@
 
 package com.android.networkstack.tethering;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
 import static android.net.TetheringManager.TETHERING_BLUETOOTH;
 import static android.net.TetheringManager.TETHERING_USB;
 import static android.net.TetheringManager.TETHERING_WIFI;
@@ -30,7 +31,7 @@
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.net.Network;
+import android.net.NetworkCapabilities;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -50,6 +51,9 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * A class to display tethering-related notifications.
  *
@@ -82,6 +86,9 @@
     // Id to update and cancel no upstream notification. Must be unique within the tethering app.
     @VisibleForTesting
     static final int NO_UPSTREAM_NOTIFICATION_ID = 1002;
+    // Id to update and cancel roaming notification. Must be unique within the tethering app.
+    @VisibleForTesting
+    static final int ROAMING_NOTIFICATION_ID = 1003;
     @VisibleForTesting
     static final int NO_ICON_ID = 0;
     @VisibleForTesting
@@ -95,13 +102,14 @@
     private final Handler mHandler;
 
     // WARNING : the constructor is called on a different thread. Thread safety therefore
-    // relies on these values being initialized to 0 or false, and not any other value. If you need
-    // to change this, you will need to change the thread where the constructor is invoked,
-    // or to introduce synchronization.
+    // relies on these values being initialized to 0, false or null, and not any other value. If you
+    // need to change this, you will need to change the thread where the constructor is invoked, or
+    // to introduce synchronization.
     // Downstream type is one of ConnectivityManager.TETHERING_* constants, 0 1 or 2.
     // This value has to be made 1 2 and 4, and OR'd with the others.
     private int mDownstreamTypesMask = DOWNSTREAM_NONE;
     private boolean mNoUpstream = false;
+    private boolean mRoaming = false;
 
     // WARNING : this value is not able to being initialized to 0 and must have volatile because
     // telephony service is not guaranteed that is up before tethering service starts. If telephony
@@ -110,7 +118,13 @@
     // INVALID_SUBSCRIPTION_ID.
     private volatile int mActiveDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
-    @IntDef({ENABLE_NOTIFICATION_ID, RESTRICTED_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID})
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            ENABLE_NOTIFICATION_ID,
+            RESTRICTED_NOTIFICATION_ID,
+            NO_UPSTREAM_NOTIFICATION_ID,
+            ROAMING_NOTIFICATION_ID
+    })
     @interface NotificationId {}
 
     private static final class MccMncOverrideInfo {
@@ -160,26 +174,22 @@
 
     /** Called when downstream has changed */
     public void onDownstreamChanged(@IntRange(from = 0, to = 7) final int downstreamTypesMask) {
-        if (mDownstreamTypesMask == downstreamTypesMask) return;
-        mDownstreamTypesMask = downstreamTypesMask;
-        updateEnableNotification();
-        updateNoUpstreamNotification();
+        updateActiveNotifications(
+                mActiveDataSubId, downstreamTypesMask, mNoUpstream, mRoaming);
     }
 
     /** Called when active data subscription id changed */
     public void onActiveDataSubscriptionIdChanged(final int subId) {
-        if (mActiveDataSubId == subId) return;
-        mActiveDataSubId = subId;
-        updateEnableNotification();
-        updateNoUpstreamNotification();
+        updateActiveNotifications(subId, mDownstreamTypesMask, mNoUpstream, mRoaming);
     }
 
-    /** Called when upstream network changed */
-    public void onUpstreamNetworkChanged(@Nullable final Network network) {
-        final boolean isNoUpstream = (network == null);
-        if (mNoUpstream == isNoUpstream) return;
-        mNoUpstream = isNoUpstream;
-        updateNoUpstreamNotification();
+    /** Called when upstream network capabilities changed */
+    public void onUpstreamCapabilitiesChanged(@Nullable final NetworkCapabilities capabilities) {
+        final boolean isNoUpstream = (capabilities == null);
+        final boolean isRoaming = capabilities != null
+                && !capabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING);
+        updateActiveNotifications(
+                mActiveDataSubId, mDownstreamTypesMask, isNoUpstream, isRoaming);
     }
 
     @NonNull
@@ -208,6 +218,25 @@
         return res;
     }
 
+    private void updateActiveNotifications(final int subId, final int downstreamTypes,
+            final boolean noUpstream, final boolean isRoaming) {
+        final boolean tetheringActiveChanged =
+                (downstreamTypes == DOWNSTREAM_NONE) != (mDownstreamTypesMask == DOWNSTREAM_NONE);
+        final boolean subIdChanged = subId != mActiveDataSubId;
+        final boolean downstreamChanged = downstreamTypes != mDownstreamTypesMask;
+        final boolean upstreamChanged = noUpstream != mNoUpstream;
+        final boolean roamingChanged = isRoaming != mRoaming;
+        final boolean updateAll = tetheringActiveChanged || subIdChanged;
+        mActiveDataSubId = subId;
+        mDownstreamTypesMask = downstreamTypes;
+        mNoUpstream = noUpstream;
+        mRoaming = isRoaming;
+
+        if (updateAll || downstreamChanged) updateEnableNotification();
+        if (updateAll || upstreamChanged) updateNoUpstreamNotification();
+        if (updateAll || roamingChanged) updateRoamingNotification();
+    }
+
     private void updateEnableNotification() {
         final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
 
@@ -219,14 +248,20 @@
     private void updateNoUpstreamNotification() {
         final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
 
-        if (tetheringInactive
-                || !mNoUpstream
-                || setupNoUpstreamNotification() == NO_NOTIFY) {
+        if (tetheringInactive || !mNoUpstream || setupNoUpstreamNotification() == NO_NOTIFY) {
             clearNotification(NO_UPSTREAM_NOTIFICATION_ID);
             mHandler.removeMessages(EVENT_SHOW_NO_UPSTREAM);
         }
     }
 
+    private void updateRoamingNotification() {
+        final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
+
+        if (tetheringInactive || !mRoaming || setupRoamingNotification() == NO_NOTIFY) {
+            clearNotification(ROAMING_NOTIFICATION_ID);
+        }
+    }
+
     @VisibleForTesting
     void tetheringRestrictionLifted() {
         clearNotification(RESTRICTED_NOTIFICATION_ID);
@@ -333,6 +368,29 @@
         return icons;
     }
 
+    private boolean setupRoamingNotification() {
+        final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
+        final boolean upstreamRoamingNotification =
+                res.getBoolean(R.bool.config_upstream_roaming_notification);
+
+        if (!upstreamRoamingNotification) return NO_NOTIFY;
+
+        final String title = res.getString(R.string.upstream_roaming_notification_title);
+        final String message = res.getString(R.string.upstream_roaming_notification_message);
+        if (isEmpty(title) || isEmpty(message)) return NO_NOTIFY;
+
+        final PendingIntent pi = PendingIntent.getActivity(
+                mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
+                0 /* requestCode */,
+                new Intent(Settings.ACTION_TETHER_SETTINGS),
+                Intent.FLAG_ACTIVITY_NEW_TASK,
+                null /* options */);
+
+        showNotification(R.drawable.stat_sys_tether_general, title, message,
+                ROAMING_NOTIFICATION_ID, pi, new Action[0]);
+        return NOTIFY_DONE;
+    }
+
     private boolean setupNoUpstreamNotification() {
         final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
         final int delayToShowUpstreamNotification =
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt
index 294bf1b..04f31a7 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt
@@ -23,10 +23,11 @@
 import android.net.ConnectivityManager.TETHERING_BLUETOOTH
 import android.net.ConnectivityManager.TETHERING_USB
 import android.net.ConnectivityManager.TETHERING_WIFI
-import android.net.Network
 import android.os.Handler
 import android.os.HandlerThread
 import android.os.Looper
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
 import android.os.UserHandle
 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import android.telephony.TelephonyManager
@@ -39,6 +40,7 @@
 import com.android.networkstack.tethering.TetheringNotificationUpdater.EVENT_SHOW_NO_UPSTREAM
 import com.android.networkstack.tethering.TetheringNotificationUpdater.NO_UPSTREAM_NOTIFICATION_ID
 import com.android.networkstack.tethering.TetheringNotificationUpdater.RESTRICTED_NOTIFICATION_ID
+import com.android.networkstack.tethering.TetheringNotificationUpdater.ROAMING_NOTIFICATION_ID
 import com.android.networkstack.tethering.TetheringNotificationUpdater.VERIZON_CARRIER_ID
 import com.android.testutils.waitForIdle
 import org.junit.After
@@ -75,6 +77,8 @@
 const val TEST_NO_UPSTREAM_TITLE = "Hotspot has no internet access"
 const val TEST_NO_UPSTREAM_MESSAGE = "Device cannot connect to internet."
 const val TEST_NO_UPSTREAM_BUTTON = "Turn off hotspot"
+const val TEST_ROAMING_TITLE = "Hotspot is on"
+const val TEST_ROAMING_MESSAGE = "Additional charges may apply while roaming."
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
@@ -98,6 +102,11 @@
             "WIFI|BT;android.test:drawable/general", "WIFI|USB;android.test:drawable/general",
             "USB|BT;android.test:drawable/general", "WIFI|USB|BT;android.test:drawable/general")
 
+    private val ROAMING_CAPABILITIES = NetworkCapabilities()
+    private val HOME_CAPABILITIES = NetworkCapabilities().addCapability(NET_CAPABILITY_NOT_ROAMING)
+    private val NOTIFICATION_ICON_ID = R.drawable.stat_sys_tether_general
+    private val TIMEOUT_MS = 500L
+
     private inner class TestContext(c: Context) : BroadcastInterceptingContext(c) {
         override fun createContextAsUser(user: UserHandle, flags: Int) =
                 if (user == UserHandle.ALL) mockContext else this
@@ -123,6 +132,8 @@
                 .getStringArray(R.array.tethering_notification_icons)
         doReturn(5).`when`(testResources)
                 .getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul)
+        doReturn(true).`when`(testResources)
+                .getBoolean(R.bool.config_upstream_roaming_notification)
         doReturn(TITLE).`when`(defaultResources).getString(R.string.tethering_notification_title)
         doReturn(MESSAGE).`when`(defaultResources)
                 .getString(R.string.tethering_notification_message)
@@ -135,6 +146,10 @@
                 .getString(R.string.no_upstream_notification_message)
         doReturn(TEST_NO_UPSTREAM_BUTTON).`when`(testResources)
                 .getString(R.string.no_upstream_notification_disable_button)
+        doReturn(TEST_ROAMING_TITLE).`when`(testResources)
+                .getString(R.string.upstream_roaming_notification_title)
+        doReturn(TEST_ROAMING_MESSAGE).`when`(testResources)
+                .getString(R.string.upstream_roaming_notification_message)
         doReturn(USB_ICON_ID).`when`(defaultResources)
                 .getIdentifier(eq("android.test:drawable/usb"), any(), any())
         doReturn(BT_ICON_ID).`when`(defaultResources)
@@ -176,41 +191,25 @@
         assertEquals(iconId, notification.smallIcon.resId)
         assertEquals(title, notification.title())
         assertEquals(text, notification.text())
-    }
 
-    private fun verifyNotificationCancelled(id: Int) =
-        verify(notificationManager, times(1)).cancel(any(), eq(id))
-
-    private val tetheringActiveNotifications =
-            listOf(NO_UPSTREAM_NOTIFICATION_ID, ENABLE_NOTIFICATION_ID)
-
-    private fun verifyCancelAllTetheringActiveNotifications() {
-        tetheringActiveNotifications.forEach {
-            verifyNotificationCancelled(it)
-        }
         reset(notificationManager)
     }
 
-    private fun verifyOnlyTetheringActiveNotification(
-        notifyId: Int,
-        iconId: Int,
-        title: String,
-        text: String
+    private fun verifyNotificationCancelled(
+        notificationIds: List<Int>,
+        resetAfterVerified: Boolean = true
     ) {
-        tetheringActiveNotifications.forEach {
-            when (it) {
-                notifyId -> verifyNotification(iconId, title, text, notifyId)
-                else -> verifyNotificationCancelled(it)
-            }
+        notificationIds.forEach {
+            verify(notificationManager, times(1)).cancel(any(), eq(it))
         }
-        reset(notificationManager)
+        if (resetAfterVerified) reset(notificationManager)
     }
 
     @Test
     fun testNotificationWithDownstreamChanged() {
         // Wifi downstream. No notification.
         notificationUpdater.onDownstreamChanged(WIFI_MASK)
-        verifyCancelAllTetheringActiveNotifications()
+        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID))
 
         // Same downstream changed. Nothing happened.
         notificationUpdater.onDownstreamChanged(WIFI_MASK)
@@ -218,23 +217,23 @@
 
         // Wifi and usb downstreams. Show enable notification
         notificationUpdater.onDownstreamChanged(WIFI_MASK or USB_MASK)
-        verifyOnlyTetheringActiveNotification(
-                ENABLE_NOTIFICATION_ID, GENERAL_ICON_ID, TITLE, MESSAGE)
+        verifyNotification(GENERAL_ICON_ID, TITLE, MESSAGE, ENABLE_NOTIFICATION_ID)
 
         // Usb downstream. Still show enable notification.
         notificationUpdater.onDownstreamChanged(USB_MASK)
-        verifyOnlyTetheringActiveNotification(ENABLE_NOTIFICATION_ID, USB_ICON_ID, TITLE, MESSAGE)
+        verifyNotification(USB_ICON_ID, TITLE, MESSAGE, ENABLE_NOTIFICATION_ID)
 
         // No downstream. No notification.
         notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
-        verifyCancelAllTetheringActiveNotifications()
+        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
+                ROAMING_NOTIFICATION_ID))
     }
 
     @Test
     fun testNotificationWithActiveDataSubscriptionIdChanged() {
         // Usb downstream. Showed enable notification with default resource.
         notificationUpdater.onDownstreamChanged(USB_MASK)
-        verifyOnlyTetheringActiveNotification(ENABLE_NOTIFICATION_ID, USB_ICON_ID, TITLE, MESSAGE)
+        verifyNotification(USB_ICON_ID, TITLE, MESSAGE, ENABLE_NOTIFICATION_ID)
 
         // Same subId changed. Nothing happened.
         notificationUpdater.onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID)
@@ -242,16 +241,17 @@
 
         // Set test sub id. Clear notification with test resource.
         notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
-        verifyCancelAllTetheringActiveNotifications()
+        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
+                ROAMING_NOTIFICATION_ID))
 
         // Wifi downstream. Show enable notification with test resource.
         notificationUpdater.onDownstreamChanged(WIFI_MASK)
-        verifyOnlyTetheringActiveNotification(
-                ENABLE_NOTIFICATION_ID, WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE)
+        verifyNotification(WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE, ENABLE_NOTIFICATION_ID)
 
         // No downstream. No notification.
         notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
-        verifyCancelAllTetheringActiveNotifications()
+        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
+                ROAMING_NOTIFICATION_ID))
     }
 
     private fun assertIconNumbers(number: Int, configs: Array<String?>) {
@@ -305,24 +305,21 @@
 
         // User restrictions on. Show restricted notification.
         notificationUpdater.notifyTetheringDisabledByRestriction()
-        verifyNotification(R.drawable.stat_sys_tether_general, title, message,
-                RESTRICTED_NOTIFICATION_ID)
-        reset(notificationManager)
+        verifyNotification(NOTIFICATION_ICON_ID, title, message, RESTRICTED_NOTIFICATION_ID)
 
         // User restrictions off. Clear notification.
         notificationUpdater.tetheringRestrictionLifted()
-        verifyNotificationCancelled(RESTRICTED_NOTIFICATION_ID)
-        reset(notificationManager)
+        verifyNotificationCancelled(listOf(RESTRICTED_NOTIFICATION_ID))
 
         // Set test sub id. No notification.
         notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
-        verifyCancelAllTetheringActiveNotifications()
+        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
+                ROAMING_NOTIFICATION_ID))
 
         // User restrictions on again. Show restricted notification with test resource.
         notificationUpdater.notifyTetheringDisabledByRestriction()
-        verifyNotification(R.drawable.stat_sys_tether_general, disallowTitle, disallowMessage,
+        verifyNotification(NOTIFICATION_ICON_ID, disallowTitle, disallowMessage,
                 RESTRICTED_NOTIFICATION_ID)
-        reset(notificationManager)
     }
 
     val MAX_BACKOFF_MS = 200L
@@ -359,51 +356,53 @@
     }
 
     @Test
-    fun testNotificationWithUpstreamNetworkChanged() {
+    fun testNotificationWithUpstreamCapabilitiesChanged_NoUpstream() {
         // Set test sub id. No notification.
         notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
-        verifyCancelAllTetheringActiveNotifications()
+        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
+                ROAMING_NOTIFICATION_ID))
 
         // Wifi downstream. Show enable notification with test resource.
         notificationUpdater.onDownstreamChanged(WIFI_MASK)
-        verifyOnlyTetheringActiveNotification(
-                ENABLE_NOTIFICATION_ID, WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE)
+        verifyNotification(WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE, ENABLE_NOTIFICATION_ID)
 
         // There is no upstream. Show no upstream notification.
-        notificationUpdater.onUpstreamNetworkChanged(null)
-        notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, 500L)
-        verifyNotification(R.drawable.stat_sys_tether_general, TEST_NO_UPSTREAM_TITLE,
-                TEST_NO_UPSTREAM_MESSAGE, NO_UPSTREAM_NOTIFICATION_ID)
-        reset(notificationManager)
+        notificationUpdater.onUpstreamCapabilitiesChanged(null)
+        notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS)
+        verifyNotification(NOTIFICATION_ICON_ID, TEST_NO_UPSTREAM_TITLE, TEST_NO_UPSTREAM_MESSAGE,
+                NO_UPSTREAM_NOTIFICATION_ID)
 
-        // Same upstream network changed. Nothing happened.
-        notificationUpdater.onUpstreamNetworkChanged(null)
+        // Same capabilities changed. Nothing happened.
+        notificationUpdater.onUpstreamCapabilitiesChanged(null)
         verifyZeroInteractions(notificationManager)
 
         // Upstream come back. Clear no upstream notification.
-        notificationUpdater.onUpstreamNetworkChanged(Network(1000))
-        verifyNotificationCancelled(NO_UPSTREAM_NOTIFICATION_ID)
-        reset(notificationManager)
+        notificationUpdater.onUpstreamCapabilitiesChanged(HOME_CAPABILITIES)
+        verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID))
 
         // No upstream again. Show no upstream notification.
-        notificationUpdater.onUpstreamNetworkChanged(null)
-        notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, 500L)
-        verifyNotification(R.drawable.stat_sys_tether_general, TEST_NO_UPSTREAM_TITLE,
-                TEST_NO_UPSTREAM_MESSAGE, NO_UPSTREAM_NOTIFICATION_ID)
-        reset(notificationManager)
+        notificationUpdater.onUpstreamCapabilitiesChanged(null)
+        notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS)
+        verifyNotification(NOTIFICATION_ICON_ID, TEST_NO_UPSTREAM_TITLE, TEST_NO_UPSTREAM_MESSAGE,
+                NO_UPSTREAM_NOTIFICATION_ID)
 
         // No downstream. No notification.
         notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
-        verifyCancelAllTetheringActiveNotifications()
+        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
+                ROAMING_NOTIFICATION_ID))
 
-        // Set R.integer.delay_to_show_no_upstream_after_no_backhaul to 0 and have wifi downstream
-        // again. Show enable notification only.
+        // Put up enable notification with wifi downstream and home capabilities.
+        notificationUpdater.onDownstreamChanged(WIFI_MASK)
+        notificationUpdater.onUpstreamCapabilitiesChanged(HOME_CAPABILITIES)
+        verifyNotification(WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE, ENABLE_NOTIFICATION_ID)
+
+        // Set R.integer.delay_to_show_no_upstream_after_no_backhaul to -1 and change to no upstream
+        // again. Don't put up no upstream notification.
         doReturn(-1).`when`(testResources)
                 .getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul)
-        notificationUpdater.onDownstreamChanged(WIFI_MASK)
-        notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, 500L)
-        verifyOnlyTetheringActiveNotification(
-                ENABLE_NOTIFICATION_ID, WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE)
+        notificationUpdater.onUpstreamCapabilitiesChanged(null)
+        notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS)
+        verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID))
     }
 
     @Test
@@ -428,4 +427,57 @@
         assertEquals(311, res.configuration.mcc)
         assertEquals(480, res.configuration.mnc)
     }
+
+    @Test
+    fun testNotificationWithUpstreamCapabilitiesChanged_Roaming() {
+        // Set test sub id. Clear notification.
+        notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
+        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
+                ROAMING_NOTIFICATION_ID))
+
+        // Wifi downstream. Show enable notification with test resource.
+        notificationUpdater.onDownstreamChanged(WIFI_MASK)
+        verifyNotification(WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE, ENABLE_NOTIFICATION_ID)
+
+        // Upstream capabilities changed to roaming state. Show roaming notification.
+        notificationUpdater.onUpstreamCapabilitiesChanged(ROAMING_CAPABILITIES)
+        verifyNotification(NOTIFICATION_ICON_ID, TEST_ROAMING_TITLE, TEST_ROAMING_MESSAGE,
+                ROAMING_NOTIFICATION_ID)
+
+        // Same capabilities change. Nothing happened.
+        notificationUpdater.onUpstreamCapabilitiesChanged(ROAMING_CAPABILITIES)
+        verifyZeroInteractions(notificationManager)
+
+        // Upstream capabilities changed to home state. Clear roaming notification.
+        notificationUpdater.onUpstreamCapabilitiesChanged(HOME_CAPABILITIES)
+        verifyNotificationCancelled(listOf(ROAMING_NOTIFICATION_ID))
+
+        // Upstream capabilities changed to roaming state again. Show roaming notification.
+        notificationUpdater.onUpstreamCapabilitiesChanged(ROAMING_CAPABILITIES)
+        verifyNotification(NOTIFICATION_ICON_ID, TEST_ROAMING_TITLE, TEST_ROAMING_MESSAGE,
+                ROAMING_NOTIFICATION_ID)
+
+        // No upstream. Clear roaming notification and show no upstream notification.
+        notificationUpdater.onUpstreamCapabilitiesChanged(null)
+        notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS)
+        verifyNotificationCancelled(listOf(ROAMING_NOTIFICATION_ID), false)
+        verifyNotification(NOTIFICATION_ICON_ID, TEST_NO_UPSTREAM_TITLE, TEST_NO_UPSTREAM_MESSAGE,
+                NO_UPSTREAM_NOTIFICATION_ID)
+
+        // No downstream. No notification.
+        notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
+        verifyNotificationCancelled(listOf(ENABLE_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID,
+                ROAMING_NOTIFICATION_ID))
+
+        // Wifi downstream again. Show enable notification with test resource.
+        notificationUpdater.onDownstreamChanged(WIFI_MASK)
+        verifyNotification(WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE, ENABLE_NOTIFICATION_ID)
+
+        // Set R.bool.config_upstream_roaming_notification to false and change upstream
+        // network to roaming state again. No roaming notification.
+        doReturn(false).`when`(testResources)
+                .getBoolean(R.bool.config_upstream_roaming_notification)
+        notificationUpdater.onUpstreamCapabilitiesChanged(ROAMING_CAPABILITIES)
+        verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID))
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 15e253a..28bfae0 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -48,6 +48,7 @@
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
 import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE;
+import static com.android.networkstack.tethering.UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -1700,7 +1701,29 @@
         stateMachine.chooseUpstreamType(true);
 
         verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(eq(upstreamState.network));
-        verify(mNotificationUpdater, times(1)).onUpstreamNetworkChanged(eq(upstreamState.network));
+        verify(mNotificationUpdater, times(1)).onUpstreamCapabilitiesChanged(any());
+    }
+
+    @Test
+    public void testUpstreamCapabilitiesChanged() {
+        final Tethering.TetherMasterSM stateMachine = (Tethering.TetherMasterSM)
+                mTetheringDependencies.mUpstreamNetworkMonitorMasterSM;
+        final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
+        when(mUpstreamNetworkMonitor.selectPreferredUpstreamType(any())).thenReturn(upstreamState);
+        stateMachine.chooseUpstreamType(true);
+
+        stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState);
+        // Should have two onUpstreamCapabilitiesChanged().
+        // One is called by reportUpstreamChanged(). One is called by EVENT_ON_CAPABILITIES.
+        verify(mNotificationUpdater, times(2)).onUpstreamCapabilitiesChanged(any());
+        reset(mNotificationUpdater);
+
+        // Verify that onUpstreamCapabilitiesChanged won't be called if not current upstream network
+        // capabilities changed.
+        final UpstreamNetworkState upstreamState2 = new UpstreamNetworkState(
+                upstreamState.linkProperties, upstreamState.networkCapabilities, new Network(101));
+        stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState2);
+        verify(mNotificationUpdater, never()).onUpstreamCapabilitiesChanged(any());
     }
 
     // TODO: Test that a request for hotspot mode doesn't interfere with an