[TNU05] Add no upstream notification

Reminder user of unavailable tethering status if there is no
internet access.

Bug: 147818698
Test: atest TetheringTests
Change-Id: Ic6557f9f7703337596100cd6a477fd7239217166
diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml
index 1dc8227..2b2fe45 100644
--- a/Tethering/AndroidManifest.xml
+++ b/Tethering/AndroidManifest.xml
@@ -34,11 +34,14 @@
     <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
     <uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
     <uses-permission android:name="android.permission.TETHER_PRIVILEGED" />
     <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
     <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" />
     <uses-permission android:name="android.permission.WRITE_SETTINGS" />
 
+    <protected-broadcast android:name="com.android.server.connectivity.tethering.DISABLE_TETHERING" />
+
     <application
         android:process="com.android.networkstack.process"
         android:extractNativeLibs="false"
diff --git a/Tethering/res/values-mcc204-mnc04/strings.xml b/Tethering/res/values-mcc204-mnc04/strings.xml
deleted file mode 100644
index 9dadd49..0000000
--- a/Tethering/res/values-mcc204-mnc04/strings.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 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.
--->
-<resources>
-    <!-- String for no upstream notification title [CHAR LIMIT=200] -->
-    <string name="no_upstream_notification_title">Tethering has no internet</string>
-    <!-- String for no upstream notification title [CHAR LIMIT=200] -->
-    <string name="no_upstream_notification_message">Devices can\u2019t connect</string>
-    <!-- String for no upstream notification disable button [CHAR LIMIT=200] -->
-    <string name="no_upstream_notification_disable_button">Turn off tethering</string>
-
-    <!-- String for cellular roaming notification title [CHAR LIMIT=200] -->
-    <string name="upstream_roaming_notification_title">Hotspot or tethering is on</string>
-    <!-- String for cellular roaming notification message [CHAR LIMIT=500] -->
-    <string name="upstream_roaming_notification_message">Additional charges may apply while roaming</string>
-    <!-- String for cellular roaming notification continue button [CHAR LIMIT=200] -->
-    <string name="upstream_roaming_notification_continue_button">Continue</string>
-</resources>
diff --git a/Tethering/res/values-mcc310-mnc004/config.xml b/Tethering/res/values-mcc310-mnc004/config.xml
new file mode 100644
index 0000000..8c627d5
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004/config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+<resources>
+    <!-- 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>
+</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
new file mode 100644
index 0000000..8c627d5
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480/config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+<resources>
+    <!-- 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>
+</resources>
\ No newline at end of file
diff --git a/Tethering/res/values/config.xml b/Tethering/res/values/config.xml
index 3a2084a..780a015 100644
--- a/Tethering/res/values/config.xml
+++ b/Tethering/res/values/config.xml
@@ -200,4 +200,10 @@
     <string name="tethering_notification_title">@string/tethered_notification_title</string>
     <!-- String for tether enable notification message. -->
     <string name="tethering_notification_message">@string/tethered_notification_message</string>
+
+    <!-- No upstream notification is shown when there is a downstream but no upstream that is able
+         to do the tethering. -->
+    <!-- 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>
 </resources>
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 601a9db..848f204 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -257,7 +257,7 @@
         mContext = mDeps.getContext();
         mNetd = mDeps.getINetd(mContext);
         mLooper = mDeps.getTetheringLooper();
-        mNotificationUpdater = mDeps.getNotificationUpdater(mContext);
+        mNotificationUpdater = mDeps.getNotificationUpdater(mContext, mLooper);
 
         mPublicSync = new Object();
 
@@ -337,6 +337,11 @@
         filter.addAction(ACTION_RESTRICT_BACKGROUND_CHANGED);
         mContext.registerReceiver(mStateReceiver, filter, null, mHandler);
 
+        final IntentFilter noUpstreamFilter = new IntentFilter();
+        noUpstreamFilter.addAction(TetheringNotificationUpdater.ACTION_DISABLE_TETHERING);
+        mContext.registerReceiver(
+                mStateReceiver, noUpstreamFilter, PERMISSION_MAINLINE_NETWORK_STACK, mHandler);
+
         final WifiManager wifiManager = getWifiManager();
         if (wifiManager != null) {
             wifiManager.registerSoftApCallback(mExecutor, new TetheringSoftApCallback());
@@ -855,6 +860,8 @@
             } else if (action.equals(ACTION_RESTRICT_BACKGROUND_CHANGED)) {
                 mLog.log("OBSERVED data saver changed");
                 handleDataSaverChanged();
+            } else if (action.equals(TetheringNotificationUpdater.ACTION_DISABLE_TETHERING)) {
+                untetherAll();
             }
         }
 
@@ -2011,6 +2018,7 @@
         } finally {
             mTetheringEventCallbacks.finishBroadcast();
         }
+        mNotificationUpdater.onUpstreamNetworkChanged(network);
     }
 
     private void reportConfigurationChanged(TetheringConfigurationParcel config) {
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 893c582..9b54b5f 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -106,8 +106,9 @@
     /**
      * Get a reference to the TetheringNotificationUpdater to be used by tethering.
      */
-    public TetheringNotificationUpdater getNotificationUpdater(@NonNull final Context ctx) {
-        return new TetheringNotificationUpdater(ctx);
+    public TetheringNotificationUpdater getNotificationUpdater(@NonNull final Context ctx,
+            @NonNull final Looper looper) {
+        return new TetheringNotificationUpdater(ctx, looper);
     }
 
     /**
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java b/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
index 4287056..ff83fd1 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
@@ -19,18 +19,25 @@
 import static android.net.TetheringManager.TETHERING_BLUETOOTH;
 import static android.net.TetheringManager.TETHERING_USB;
 import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.text.TextUtils.isEmpty;
 
 import android.app.Notification;
+import android.app.Notification.Action;
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.net.Network;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.telephony.SubscriptionManager;
-import android.text.TextUtils;
+import android.telephony.TelephonyManager;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -39,9 +46,13 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.Arrays;
+import java.util.List;
+
 /**
  * A class to display tethering-related notifications.
  *
@@ -58,12 +69,22 @@
     private static final String WIFI_DOWNSTREAM = "WIFI";
     private static final String USB_DOWNSTREAM = "USB";
     private static final String BLUETOOTH_DOWNSTREAM = "BT";
+    @VisibleForTesting
+    static final String ACTION_DISABLE_TETHERING =
+            "com.android.server.connectivity.tethering.DISABLE_TETHERING";
     private static final boolean NOTIFY_DONE = true;
     private static final boolean NO_NOTIFY = false;
-    // Id to update and cancel tethering notification. Must be unique within the tethering app.
-    private static final int ENABLE_NOTIFICATION_ID = 1000;
+    @VisibleForTesting
+    static final int EVENT_SHOW_NO_UPSTREAM = 1;
+    // Id to update and cancel enable notification. Must be unique within the tethering app.
+    @VisibleForTesting
+    static final int ENABLE_NOTIFICATION_ID = 1000;
     // Id to update and cancel restricted notification. Must be unique within the tethering app.
-    private static final int RESTRICTED_NOTIFICATION_ID = 1001;
+    @VisibleForTesting
+    static final int RESTRICTED_NOTIFICATION_ID = 1001;
+    // Id to update and cancel no upstream notification. Must be unique within the tethering app.
+    @VisibleForTesting
+    static final int NO_UPSTREAM_NOTIFICATION_ID = 1002;
     @VisibleForTesting
     static final int NO_ICON_ID = 0;
     @VisibleForTesting
@@ -71,14 +92,16 @@
     private final Context mContext;
     private final NotificationManager mNotificationManager;
     private final NotificationChannel mChannel;
+    private final Handler mHandler;
 
     // WARNING : the constructor is called on a different thread. Thread safety therefore
-    // relies on this value being initialized to 0, and not any other value. If you need
+    // 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.
     // 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;
 
     // 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
@@ -87,10 +110,30 @@
     // INVALID_SUBSCRIPTION_ID.
     private volatile int mActiveDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
-    @IntDef({ENABLE_NOTIFICATION_ID, RESTRICTED_NOTIFICATION_ID})
+    @IntDef({ENABLE_NOTIFICATION_ID, RESTRICTED_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID})
     @interface NotificationId {}
 
-    public TetheringNotificationUpdater(@NonNull final Context context) {
+    private static final class MccMncOverrideInfo {
+        public final List<String> visitedMccMncs;
+        public final int homeMcc;
+        public final int homeMnc;
+        MccMncOverrideInfo(List<String> visitedMccMncs, int mcc, int mnc) {
+            this.visitedMccMncs = visitedMccMncs;
+            this.homeMcc = mcc;
+            this.homeMnc = mnc;
+        }
+    }
+
+    private static final SparseArray<MccMncOverrideInfo> sCarrierIdToMccMnc = new SparseArray<>();
+
+    static {
+        // VZW
+        sCarrierIdToMccMnc.put(
+                1839, new MccMncOverrideInfo(Arrays.asList(new String[] {"20404"}), 311, 480));
+    }
+
+    public TetheringNotificationUpdater(@NonNull final Context context,
+            @NonNull final Looper looper) {
         mContext = context;
         mNotificationManager = (NotificationManager) context.createContextAsUser(UserHandle.ALL, 0)
                 .getSystemService(Context.NOTIFICATION_SERVICE);
@@ -99,6 +142,22 @@
                 context.getResources().getString(R.string.notification_channel_tethering_status),
                 NotificationManager.IMPORTANCE_LOW);
         mNotificationManager.createNotificationChannel(mChannel);
+        mHandler = new NotificationHandler(looper);
+    }
+
+    private class NotificationHandler extends Handler {
+        NotificationHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch(msg.what) {
+                case EVENT_SHOW_NO_UPSTREAM:
+                    notifyTetheringNoUpstream();
+                    break;
+            }
+        }
     }
 
     /** Called when downstream has changed */
@@ -106,6 +165,7 @@
         if (mDownstreamTypesMask == downstreamTypesMask) return;
         mDownstreamTypesMask = downstreamTypesMask;
         updateEnableNotification();
+        updateNoUpstreamNotification();
     }
 
     /** Called when active data subscription id changed */
@@ -113,21 +173,62 @@
         if (mActiveDataSubId == subId) return;
         mActiveDataSubId = subId;
         updateEnableNotification();
+        updateNoUpstreamNotification();
     }
 
+    /** Called when upstream network changed */
+    public void onUpstreamNetworkChanged(@Nullable final Network network) {
+        final boolean isNoUpstream = (network == null);
+        if (mNoUpstream == isNoUpstream) return;
+        mNoUpstream = isNoUpstream;
+        updateNoUpstreamNotification();
+    }
+
+    @NonNull
     @VisibleForTesting
-    Resources getResourcesForSubId(@NonNull final Context c, final int subId) {
-        return SubscriptionManager.getResourcesForSubId(c, subId);
+    final Handler getHandler() {
+        return mHandler;
+    }
+
+    @NonNull
+    @VisibleForTesting
+    Resources getResourcesForSubId(@NonNull final Context context, final int subId) {
+        final Resources res = SubscriptionManager.getResourcesForSubId(context, subId);
+        final TelephonyManager tm =
+                ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE))
+                        .createForSubscriptionId(mActiveDataSubId);
+        final int carrierId = tm.getSimCarrierId();
+        final String mccmnc = tm.getSimOperator();
+        final MccMncOverrideInfo overrideInfo = sCarrierIdToMccMnc.get(carrierId);
+        if (overrideInfo != null && overrideInfo.visitedMccMncs.contains(mccmnc)) {
+            // Re-configure MCC/MNC value to specific carrier to get right resources.
+            final Configuration config = res.getConfiguration();
+            config.mcc = overrideInfo.homeMcc;
+            config.mnc = overrideInfo.homeMnc;
+            return context.createConfigurationContext(config).getResources();
+        }
+        return res;
     }
 
     private void updateEnableNotification() {
-        final boolean tetheringInactive = mDownstreamTypesMask <= DOWNSTREAM_NONE;
+        final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
 
         if (tetheringInactive || setupNotification() == NO_NOTIFY) {
             clearNotification(ENABLE_NOTIFICATION_ID);
         }
     }
 
+    private void updateNoUpstreamNotification() {
+        final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
+
+        if (tetheringInactive
+                || !mNoUpstream
+                || setupNoUpstreamNotification() == NO_NOTIFY) {
+            clearNotification(NO_UPSTREAM_NOTIFICATION_ID);
+            mHandler.removeMessages(EVENT_SHOW_NO_UPSTREAM);
+        }
+    }
+
     @VisibleForTesting
     void tetheringRestrictionLifted() {
         clearNotification(RESTRICTED_NOTIFICATION_ID);
@@ -142,9 +243,38 @@
         final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
         final String title = res.getString(R.string.disable_tether_notification_title);
         final String message = res.getString(R.string.disable_tether_notification_message);
+        if (isEmpty(title) || isEmpty(message)) return;
+
+        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,
-                RESTRICTED_NOTIFICATION_ID);
+                RESTRICTED_NOTIFICATION_ID, pi, new Action[0]);
+    }
+
+    private void notifyTetheringNoUpstream() {
+        final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
+        final String title = res.getString(R.string.no_upstream_notification_title);
+        final String message = res.getString(R.string.no_upstream_notification_message);
+        final String disableButton =
+                res.getString(R.string.no_upstream_notification_disable_button);
+        if (isEmpty(title) || isEmpty(message) || isEmpty(disableButton)) return;
+
+        final Intent intent = new Intent(ACTION_DISABLE_TETHERING);
+        intent.setPackage(mContext.getPackageName());
+        final PendingIntent pi = PendingIntent.getBroadcast(
+                mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
+                0 /* requestCode */,
+                intent,
+                0 /* flags */);
+        final Action action = new Action.Builder(NO_ICON_ID, disableButton, pi).build();
+
+        showNotification(R.drawable.stat_sys_tether_general, title, message,
+                NO_UPSTREAM_NOTIFICATION_ID, null /* pendingIntent */, action);
     }
 
     /**
@@ -179,12 +309,13 @@
      *
      * @return {@link android.util.SparseArray} with downstream types and icon id info.
      */
+    @NonNull
     @VisibleForTesting
     SparseArray<Integer> getIcons(@ArrayRes int id, @NonNull Resources res) {
         final String[] array = res.getStringArray(id);
         final SparseArray<Integer> icons = new SparseArray<>();
         for (String config : array) {
-            if (TextUtils.isEmpty(config)) continue;
+            if (isEmpty(config)) continue;
 
             final String[] elements = config.split(";");
             if (elements.length != 2) {
@@ -204,6 +335,18 @@
         return icons;
     }
 
+    private boolean setupNoUpstreamNotification() {
+        final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
+        final int delayToShowUpstreamNotification =
+                res.getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul);
+
+        if (delayToShowUpstreamNotification < 0) return NO_NOTIFY;
+
+        mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_SHOW_NO_UPSTREAM),
+                delayToShowUpstreamNotification);
+        return NOTIFY_DONE;
+    }
+
     private boolean setupNotification() {
         final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
         final SparseArray<Integer> downstreamIcons =
@@ -214,17 +357,22 @@
 
         final String title = res.getString(R.string.tethering_notification_title);
         final String message = res.getString(R.string.tethering_notification_message);
+        if (isEmpty(title) || isEmpty(message)) return NO_NOTIFY;
 
-        showNotification(iconId, title, message, ENABLE_NOTIFICATION_ID);
+        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(iconId, title, message, ENABLE_NOTIFICATION_ID, pi, new Action[0]);
         return NOTIFY_DONE;
     }
 
     private void showNotification(@DrawableRes final int iconId, @NonNull final String title,
-            @NonNull final String message, @NotificationId final int id) {
-        final Intent intent = new Intent(Settings.ACTION_TETHER_SETTINGS);
-        final PendingIntent pi = PendingIntent.getActivity(
-                mContext.createContextAsUser(UserHandle.CURRENT, 0),
-                0 /* requestCode */, intent, 0 /* flags */, null /* options */);
+            @NonNull final String message, @NotificationId final int id, @Nullable PendingIntent pi,
+            @NonNull final Action... actions) {
         final Notification notification =
                 new Notification.Builder(mContext, mChannel.getId())
                         .setSmallIcon(iconId)
@@ -236,6 +384,7 @@
                         .setVisibility(Notification.VISIBILITY_PUBLIC)
                         .setCategory(Notification.CATEGORY_STATUS)
                         .setContentIntent(pi)
+                        .setActions(actions)
                         .build();
 
         mNotificationManager.notify(null /* tag */, id, notification);
diff --git a/Tethering/tests/unit/AndroidManifest.xml b/Tethering/tests/unit/AndroidManifest.xml
index 55640db..31eaabf 100644
--- a/Tethering/tests/unit/AndroidManifest.xml
+++ b/Tethering/tests/unit/AndroidManifest.xml
@@ -16,6 +16,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.networkstack.tethering.tests.unit">
 
+    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
     <uses-permission android:name="android.permission.TETHER_PRIVILEGED"/>
 
     <application android:debuggable="true">
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 7bff74b..5f88588 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt
@@ -23,14 +23,26 @@
 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.os.UserHandle
 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
-import androidx.test.platform.app.InstrumentationRegistry
+import android.telephony.TelephonyManager
 import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.runner.AndroidJUnit4
 import com.android.internal.util.test.BroadcastInterceptingContext
 import com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE
+import com.android.networkstack.tethering.TetheringNotificationUpdater.ENABLE_NOTIFICATION_ID
+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.testutils.waitForIdle
+import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -43,8 +55,8 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.times
-import org.mockito.Mockito.verifyZeroInteractions
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
 import org.mockito.MockitoAnnotations
 
 const val TEST_SUBID = 1
@@ -55,10 +67,13 @@
 const val WIFI_MASK = 1 shl TETHERING_WIFI
 const val USB_MASK = 1 shl TETHERING_USB
 const val BT_MASK = 1 shl TETHERING_BLUETOOTH
-const val TITTLE = "Tethering active"
+const val TITLE = "Tethering active"
 const val MESSAGE = "Tap here to set up."
-const val TEST_TITTLE = "Hotspot active"
+const val TEST_TITLE = "Hotspot active"
 const val TEST_MESSAGE = "Tap to set up hotspot."
+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"
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
@@ -67,12 +82,15 @@
     // should crash if they are used before being initialized.
     @Mock private lateinit var mockContext: Context
     @Mock private lateinit var notificationManager: NotificationManager
+    @Mock private lateinit var telephonyManager: TelephonyManager
     @Mock private lateinit var defaultResources: Resources
     @Mock private lateinit var testResources: Resources
 
-    // lateinit for this class under test, as it should be reset to a different instance for every
-    // tests but should always be initialized before use (or the test should crash).
+    // lateinit for these classes under test, as they should be reset to a different instance for
+    // every test but should always be initialized before use (or the test should crash).
+    private lateinit var context: TestContext
     private lateinit var notificationUpdater: TetheringNotificationUpdater
+    private lateinit var fakeTetheringThread: HandlerThread
 
     private val ENABLE_ICON_CONFIGS = arrayOf(
             "USB;android.test:drawable/usb", "BT;android.test:drawable/bluetooth",
@@ -82,11 +100,19 @@
     private inner class TestContext(c: Context) : BroadcastInterceptingContext(c) {
         override fun createContextAsUser(user: UserHandle, flags: Int) =
                 if (user == UserHandle.ALL) mockContext else this
+        override fun getSystemService(name: String) =
+                if (name == Context.TELEPHONY_SERVICE) telephonyManager
+                else super.getSystemService(name)
     }
 
-    private inner class WrappedNotificationUpdater(c: Context) : TetheringNotificationUpdater(c) {
+    private inner class WrappedNotificationUpdater(c: Context, looper: Looper)
+        : TetheringNotificationUpdater(c, looper) {
         override fun getResourcesForSubId(context: Context, subId: Int) =
-                if (subId == TEST_SUBID) testResources else defaultResources
+                when (subId) {
+                    TEST_SUBID -> testResources
+                    INVALID_SUBSCRIPTION_ID -> defaultResources
+                    else -> super.getResourcesForSubId(context, subId)
+                }
     }
 
     private fun setupResources() {
@@ -94,12 +120,20 @@
                 .getStringArray(R.array.tethering_notification_icons)
         doReturn(arrayOf("WIFI;android.test:drawable/wifi")).`when`(testResources)
                 .getStringArray(R.array.tethering_notification_icons)
-        doReturn(TITTLE).`when`(defaultResources).getString(R.string.tethering_notification_title)
+        doReturn(5).`when`(testResources)
+                .getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul)
+        doReturn(TITLE).`when`(defaultResources).getString(R.string.tethering_notification_title)
         doReturn(MESSAGE).`when`(defaultResources)
                 .getString(R.string.tethering_notification_message)
-        doReturn(TEST_TITTLE).`when`(testResources).getString(R.string.tethering_notification_title)
+        doReturn(TEST_TITLE).`when`(testResources).getString(R.string.tethering_notification_title)
         doReturn(TEST_MESSAGE).`when`(testResources)
                 .getString(R.string.tethering_notification_message)
+        doReturn(TEST_NO_UPSTREAM_TITLE).`when`(testResources)
+                .getString(R.string.no_upstream_notification_title)
+        doReturn(TEST_NO_UPSTREAM_MESSAGE).`when`(testResources)
+                .getString(R.string.no_upstream_notification_message)
+        doReturn(TEST_NO_UPSTREAM_BUTTON).`when`(testResources)
+                .getString(R.string.no_upstream_notification_disable_button)
         doReturn(USB_ICON_ID).`when`(defaultResources)
                 .getIdentifier(eq("android.test:drawable/usb"), any(), any())
         doReturn(BT_ICON_ID).`when`(defaultResources)
@@ -113,35 +147,61 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        val context = TestContext(InstrumentationRegistry.getInstrumentation().context)
+        context = TestContext(InstrumentationRegistry.getInstrumentation().context)
         doReturn(notificationManager).`when`(mockContext)
                 .getSystemService(Context.NOTIFICATION_SERVICE)
-        notificationUpdater = WrappedNotificationUpdater(context)
+        fakeTetheringThread = HandlerThread(this::class.simpleName)
+        fakeTetheringThread.start()
+        notificationUpdater = WrappedNotificationUpdater(context, fakeTetheringThread.looper)
         setupResources()
     }
 
+    @After
+    fun tearDown() {
+        fakeTetheringThread.quitSafely()
+    }
+
     private fun Notification.title() = this.extras.getString(Notification.EXTRA_TITLE)
     private fun Notification.text() = this.extras.getString(Notification.EXTRA_TEXT)
 
-    private fun verifyNotification(iconId: Int = 0, title: String = "", text: String = "") {
-        verify(notificationManager, never()).cancel(any(), anyInt())
+    private fun verifyNotification(iconId: Int, title: String, text: String, id: Int) {
+        verify(notificationManager, never()).cancel(any(), eq(id))
 
         val notificationCaptor = ArgumentCaptor.forClass(Notification::class.java)
         verify(notificationManager, times(1))
-                .notify(any(), anyInt(), notificationCaptor.capture())
+                .notify(any(), eq(id), notificationCaptor.capture())
 
         val notification = notificationCaptor.getValue()
         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 verifyNoNotification() {
-        verify(notificationManager, times(1)).cancel(any(), anyInt())
-        verify(notificationManager, never()).notify(any(), anyInt(), any())
-
+    private fun verifyOnlyTetheringActiveNotification(
+        notifyId: Int,
+        iconId: Int,
+        title: String,
+        text: String
+    ) {
+        tetheringActiveNotifications.forEach {
+            when (it) {
+                notifyId -> verifyNotification(iconId, title, text, notifyId)
+                else -> verifyNotificationCancelled(it)
+            }
+        }
         reset(notificationManager)
     }
 
@@ -149,7 +209,7 @@
     fun testNotificationWithDownstreamChanged() {
         // Wifi downstream. No notification.
         notificationUpdater.onDownstreamChanged(WIFI_MASK)
-        verifyNoNotification()
+        verifyCancelAllTetheringActiveNotifications()
 
         // Same downstream changed. Nothing happened.
         notificationUpdater.onDownstreamChanged(WIFI_MASK)
@@ -157,22 +217,23 @@
 
         // Wifi and usb downstreams. Show enable notification
         notificationUpdater.onDownstreamChanged(WIFI_MASK or USB_MASK)
-        verifyNotification(GENERAL_ICON_ID, TITTLE, MESSAGE)
+        verifyOnlyTetheringActiveNotification(
+                ENABLE_NOTIFICATION_ID, GENERAL_ICON_ID, TITLE, MESSAGE)
 
         // Usb downstream. Still show enable notification.
         notificationUpdater.onDownstreamChanged(USB_MASK)
-        verifyNotification(USB_ICON_ID, TITTLE, MESSAGE)
+        verifyOnlyTetheringActiveNotification(ENABLE_NOTIFICATION_ID, USB_ICON_ID, TITLE, MESSAGE)
 
         // No downstream. No notification.
         notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
-        verifyNoNotification()
+        verifyCancelAllTetheringActiveNotifications()
     }
 
     @Test
     fun testNotificationWithActiveDataSubscriptionIdChanged() {
         // Usb downstream. Showed enable notification with default resource.
         notificationUpdater.onDownstreamChanged(USB_MASK)
-        verifyNotification(USB_ICON_ID, TITTLE, MESSAGE)
+        verifyOnlyTetheringActiveNotification(ENABLE_NOTIFICATION_ID, USB_ICON_ID, TITLE, MESSAGE)
 
         // Same subId changed. Nothing happened.
         notificationUpdater.onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID)
@@ -180,15 +241,16 @@
 
         // Set test sub id. Clear notification with test resource.
         notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
-        verifyNoNotification()
+        verifyCancelAllTetheringActiveNotifications()
 
         // Wifi downstream. Show enable notification with test resource.
         notificationUpdater.onDownstreamChanged(WIFI_MASK)
-        verifyNotification(WIFI_ICON_ID, TEST_TITTLE, TEST_MESSAGE)
+        verifyOnlyTetheringActiveNotification(
+                ENABLE_NOTIFICATION_ID, WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE)
 
         // No downstream. No notification.
         notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
-        verifyNoNotification()
+        verifyCancelAllTetheringActiveNotifications()
     }
 
     private fun assertIconNumbers(number: Int, configs: Array<String?>) {
@@ -227,10 +289,8 @@
 
     @Test
     fun testSetupRestrictedNotification() {
-        val title = InstrumentationRegistry.getInstrumentation().context.resources
-                .getString(R.string.disable_tether_notification_title)
-        val message = InstrumentationRegistry.getInstrumentation().context.resources
-                .getString(R.string.disable_tether_notification_message)
+        val title = context.resources.getString(R.string.disable_tether_notification_title)
+        val message = context.resources.getString(R.string.disable_tether_notification_message)
         val disallowTitle = "Tether function is disallowed"
         val disallowMessage = "Please contact your admin"
         doReturn(title).`when`(defaultResources)
@@ -244,18 +304,127 @@
 
         // User restrictions on. Show restricted notification.
         notificationUpdater.notifyTetheringDisabledByRestriction()
-        verifyNotification(R.drawable.stat_sys_tether_general, title, message)
+        verifyNotification(R.drawable.stat_sys_tether_general, title, message,
+                RESTRICTED_NOTIFICATION_ID)
+        reset(notificationManager)
 
         // User restrictions off. Clear notification.
         notificationUpdater.tetheringRestrictionLifted()
-        verifyNoNotification()
+        verifyNotificationCancelled(RESTRICTED_NOTIFICATION_ID)
+        reset(notificationManager)
 
         // Set test sub id. No notification.
         notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
-        verifyNoNotification()
+        verifyCancelAllTetheringActiveNotifications()
 
         // User restrictions on again. Show restricted notification with test resource.
         notificationUpdater.notifyTetheringDisabledByRestriction()
-        verifyNotification(R.drawable.stat_sys_tether_general, disallowTitle, disallowMessage)
+        verifyNotification(R.drawable.stat_sys_tether_general, disallowTitle, disallowMessage,
+                RESTRICTED_NOTIFICATION_ID)
+        reset(notificationManager)
+    }
+
+    val MAX_BACKOFF_MS = 200L
+    /**
+     * Waits for all messages, including delayed ones, to be processed.
+     *
+     * This will wait until the handler has no more messages to be processed including
+     * delayed ones, or the timeout has expired. It uses an exponential backoff strategy
+     * to wait longer and longer to consume less CPU, with the max granularity being
+     * MAX_BACKOFF_MS.
+     *
+     * @return true if all messages have been processed including delayed ones, false if timeout
+     *
+     * TODO: Move this method to com.android.testutils.HandlerUtils.kt.
+     */
+    private fun Handler.waitForDelayedMessage(what: Int?, timeoutMs: Long) {
+        fun hasMatchingMessages() =
+                if (what == null) hasMessagesOrCallbacks() else hasMessages(what)
+        val expiry = System.currentTimeMillis() + timeoutMs
+        var delay = 5L
+        while (System.currentTimeMillis() < expiry && hasMatchingMessages()) {
+            // None of Handler, Looper, Message and MessageQueue expose any way to retrieve
+            // the time when the next (let alone the last) message will be processed, so
+            // short of examining the internals with reflection sleep() is the only solution.
+            Thread.sleep(delay)
+            delay = (delay * 2)
+                    .coerceAtMost(expiry - System.currentTimeMillis())
+                    .coerceAtMost(MAX_BACKOFF_MS)
+        }
+
+        val timeout = expiry - System.currentTimeMillis()
+        if (timeout <= 0) fail("Delayed message did not process yet after ${timeoutMs}ms")
+        waitForIdle(timeout)
+    }
+
+    @Test
+    fun testNotificationWithUpstreamNetworkChanged() {
+        // Set test sub id. No notification.
+        notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
+        verifyCancelAllTetheringActiveNotifications()
+
+        // Wifi downstream. Show enable notification with test resource.
+        notificationUpdater.onDownstreamChanged(WIFI_MASK)
+        verifyOnlyTetheringActiveNotification(
+                ENABLE_NOTIFICATION_ID, WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE)
+
+        // 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)
+
+        // Same upstream network changed. Nothing happened.
+        notificationUpdater.onUpstreamNetworkChanged(null)
+        verifyZeroInteractions(notificationManager)
+
+        // Upstream come back. Clear no upstream notification.
+        notificationUpdater.onUpstreamNetworkChanged(Network(1000))
+        verifyNotificationCancelled(NO_UPSTREAM_NOTIFICATION_ID)
+        reset(notificationManager)
+
+        // 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)
+
+        // No downstream. No notification.
+        notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
+        verifyCancelAllTetheringActiveNotifications()
+
+        // Set R.integer.delay_to_show_no_upstream_after_no_backhaul to 0 and have wifi downstream
+        // again. Show enable notification only.
+        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)
+    }
+
+    @Test
+    fun testGetResourcesForSubId() {
+        doReturn(telephonyManager).`when`(telephonyManager).createForSubscriptionId(anyInt())
+        doReturn(1234).`when`(telephonyManager).getSimCarrierId()
+        doReturn("000000").`when`(telephonyManager).getSimOperator()
+
+        val subId = -2 // Use invalid subId to avoid getting resource from cache or real subId.
+        val config = context.resources.configuration
+        var res = notificationUpdater.getResourcesForSubId(context, subId)
+        assertEquals(config.mcc, res.configuration.mcc)
+        assertEquals(config.mnc, res.configuration.mnc)
+
+        doReturn(1839).`when`(telephonyManager).getSimCarrierId()
+        res = notificationUpdater.getResourcesForSubId(context, subId)
+        assertEquals(config.mcc, res.configuration.mcc)
+        assertEquals(config.mnc, res.configuration.mnc)
+
+        doReturn("20404").`when`(telephonyManager).getSimOperator()
+        res = notificationUpdater.getResourcesForSubId(context, subId)
+        assertEquals(311, res.configuration.mcc)
+        assertEquals(480, res.configuration.mnc)
     }
 }
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 5bec513..15e253a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -383,7 +383,7 @@
         }
 
         @Override
-        public TetheringNotificationUpdater getNotificationUpdater(Context ctx) {
+        public TetheringNotificationUpdater getNotificationUpdater(Context ctx, Looper looper) {
             return mNotificationUpdater;
         }
     }
@@ -1691,6 +1691,18 @@
         assertEquals(clientAddrParceled, params.clientAddr);
     }
 
+    @Test
+    public void testUpstreamNetworkChanged() {
+        final Tethering.TetherMasterSM stateMachine = (Tethering.TetherMasterSM)
+                mTetheringDependencies.mUpstreamNetworkMonitorMasterSM;
+        final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
+        when(mUpstreamNetworkMonitor.selectPreferredUpstreamType(any())).thenReturn(upstreamState);
+        stateMachine.chooseUpstreamType(true);
+
+        verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(eq(upstreamState.network));
+        verify(mNotificationUpdater, times(1)).onUpstreamNetworkChanged(eq(upstreamState.network));
+    }
+
     // TODO: Test that a request for hotspot mode doesn't interfere with an
     // already operating tethering mode interface.
 }