Support notifying network switches via notifications and toasts.

Bug: 31025214
Change-Id: If1578f422f38a1dcfaec529882aed01ae5e8d53c
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 82c9300..b80b0a7 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -268,6 +268,13 @@
     <!-- The maximum duration (in milliseconds) we expect a network transition to take -->
     <integer name="config_networkTransitionTimeout">60000</integer>
 
+    <!-- Whether/how to notify the user on network switches. See LingerMonitor.java. -->
+    <integer translatable="false" name="config_networkNotifySwitchType">0</integer>
+
+    <!-- What types of network switches to notify. See LingerMonitor.java. -->
+    <string-array translatable="false" name="config_networkNotifySwitches">
+    </string-array>
+
     <!-- List of regexpressions describing the interface (if any) that represent tetherable
          USB interfaces.  If the device doesn't want to support tethering over USB this should
          be empty.  An example would be "usb.*" -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index fbd57df..9002ed4 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -2929,6 +2929,15 @@
     <!-- A notification is shown when the user connects to a Wi-Fi network and the system detects that that network has no Internet access. This is the notification's message. -->
     <string name="wifi_no_internet_detailed">Tap for options</string>
 
+    <!-- A notification might be shown if the device switches to another network type (e.g., cellular data) because it detects that the network it was using (e.g., Wi-Fi) has lost Internet connectivity. This is the notification's title. %1$s is the network type that the device switched to, e.g., cellular data. It is one of the strings in the network_switch_type_name array. -->
+    <string name="network_switch_metered">Switched to <xliff:g id="network_type">%1$s</xliff:g></string>
+
+    <!-- A notification might be shown if the device switches to another network type (e.g., cellular data) because it detects that the network it was using (e.g., Wi-Fi) has lost Internet connectivity. This is the notification's message. %1$s is the network that the device switched to, e.g., cellular data. %2$s is the network type the device switched from, e.g., Wi-Fi. Both are strings in the network_switch_type_name array. -->
+    <string name="network_switch_metered_detail">Device uses <xliff:g id="new_network">%1$s</xliff:g> when <xliff:g id="previous_network">%2$s</xliff:g> has no Internet access. Charges may apply.</string>
+
+    <!-- A toast might be shown if the device switches to another network type (e.g., cellular data) because it detects that the network it was using (e.g., Wi-Fi) has lost Internet connectivity. This is the text of the toast. %1$s is the network that the device switched from, e.g., Wi-Fi. %2$s is the network type the device switched from, e.g., cellular data. Both are strings in the network_switch_type_name array. -->
+    <string name="network_switch_metered_toast">Switched from <xliff:g id="previous_network">%1$s</xliff:g> to <xliff:g id="new_network">%2$s</xliff:g></string>
+
     <!-- Network type names used in the network_switch_metered and network_switch_metered_detail strings. These must be kept in the sync with the values NetworkCapabilities.TRANSPORT_xxx values, and in the same order. -->
     <string-array name="network_switch_type_name">
         <item>cellular data</item>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 11e83d8..924d338 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -947,6 +947,9 @@
   <java-symbol type="string" name="wifi_available_sign_in" />
   <java-symbol type="string" name="network_available_sign_in" />
   <java-symbol type="string" name="network_available_sign_in_detailed" />
+  <java-symbol type="string" name="network_switch_metered" />
+  <java-symbol type="string" name="network_switch_metered_detail" />
+  <java-symbol type="string" name="network_switch_metered_toast" />
   <java-symbol type="array" name="network_switch_type_name" />
   <java-symbol type="string" name="network_switch_type_name_unknown" />
   <java-symbol type="string" name="wifi_no_internet" />
@@ -1741,6 +1744,8 @@
   <java-symbol type="integer" name="config_lowBatteryWarningLevel" />
   <java-symbol type="integer" name="config_networkPolicyDefaultWarning" />
   <java-symbol type="integer" name="config_networkTransitionTimeout" />
+  <java-symbol type="integer" name="config_networkNotifySwitchType" />
+  <java-symbol type="array" name="config_networkNotifySwitches" />
   <java-symbol type="integer" name="config_notificationsBatteryFullARGB" />
   <java-symbol type="integer" name="config_notificationsBatteryLedOff" />
   <java-symbol type="integer" name="config_notificationsBatteryLedOn" />
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 0793832..14243c5 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -131,6 +131,7 @@
 import com.android.server.connectivity.DataConnectionStats;
 import com.android.server.connectivity.KeepaliveTracker;
 import com.android.server.connectivity.Nat464Xlat;
+import com.android.server.connectivity.LingerMonitor;
 import com.android.server.connectivity.NetworkAgentInfo;
 import com.android.server.connectivity.NetworkDiagnostics;
 import com.android.server.connectivity.NetworkMonitor;
@@ -438,6 +439,7 @@
 
     private KeepaliveTracker mKeepaliveTracker;
     private NetworkNotificationManager mNotifier;
+    private LingerMonitor mLingerMonitor;
 
     // sequence number for Networks; keep in sync with system/netd/NetworkController.cpp
     private final static int MIN_NET_ID = 100; // some reserved marks
@@ -836,6 +838,7 @@
         mKeepaliveTracker = new KeepaliveTracker(mHandler);
         mNotifier = new NetworkNotificationManager(mContext, mTelephonyManager,
                 mContext.getSystemService(NotificationManager.class));
+        mLingerMonitor = new LingerMonitor(mContext, mNotifier);
     }
 
     private NetworkRequest createInternetRequestForTransport(int transportType) {
@@ -2241,7 +2244,7 @@
                             break;
                         }
                         if (!nai.networkMisc.provisioningNotificationDisabled) {
-                            mNotifier.showNotification(netId, NotificationType.SIGN_IN, nai,
+                            mNotifier.showNotification(netId, NotificationType.SIGN_IN, nai, null,
                                     (PendingIntent) msg.obj, nai.networkMisc.explicitlySelected);
                         }
                     }
@@ -2392,6 +2395,7 @@
             }
             mLegacyTypeTracker.remove(nai, wasDefault);
             rematchAllNetworksAndRequests(null, 0);
+            mLingerMonitor.noteDisconnect(nai);
             if (nai.created) {
                 // Tell netd to clean up the configuration for this network
                 // (routing rules, DNS, etc).
@@ -2713,7 +2717,7 @@
         PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
                 mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
 
-        mNotifier.showNotification(nai.network.netId, NotificationType.NO_INTERNET, nai,
+        mNotifier.showNotification(nai.network.netId, NotificationType.NO_INTERNET, nai, null,
                 pendingIntent, true);
     }
 
@@ -4249,6 +4253,10 @@
         return nai == getDefaultNetwork();
     }
 
+    private boolean isDefaultRequest(NetworkRequestInfo nri) {
+        return nri.request.requestId == mDefaultRequest.requestId;
+    }
+
     public int registerNetworkAgent(Messenger messenger, NetworkInfo networkInfo,
             LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
             int currentScore, NetworkMisc networkMisc) {
@@ -4691,6 +4699,9 @@
                         if (VDBG) log("   accepting network in place of " + currentNetwork.name());
                         currentNetwork.removeRequest(nri.request.requestId);
                         currentNetwork.lingerRequest(nri.request, now, mLingerDelayMs);
+                        if (isDefaultRequest(nri)) {
+                            mLingerMonitor.noteLingerDefaultNetwork(currentNetwork, newNetwork);
+                        }
                         affectedNetworks.add(currentNetwork);
                     } else {
                         if (VDBG) log("   accepting network in place of null");
@@ -4708,7 +4719,7 @@
                     // network.  Think about if there is a way to reduce this.  Push
                     // netid->request mapping to each factory?
                     sendUpdatedScoreToFactories(nri.request, newNetwork.getCurrentScore());
-                    if (mDefaultRequest.requestId == nri.request.requestId) {
+                    if (isDefaultRequest(nri)) {
                         isNewDefault = true;
                         oldDefaultNetwork = currentNetwork;
                     }
diff --git a/services/core/java/com/android/server/connectivity/LingerMonitor.java b/services/core/java/com/android/server/connectivity/LingerMonitor.java
new file mode 100644
index 0000000..4034877
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/LingerMonitor.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.app.PendingIntent;
+import android.net.ConnectivityManager;
+import android.net.NetworkCapabilities;
+import android.net.Uri;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.util.SparseBooleanArray;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import com.android.internal.util.MessageUtils;
+import com.android.server.connectivity.NetworkNotificationManager;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+
+import static android.net.ConnectivityManager.NETID_UNSET;
+
+/**
+ * Class that monitors default network linger events and possibly notifies the user of network
+ * switches.
+ *
+ * This class is not thread-safe and all its methods must be called on the ConnectivityService
+ * handler thread.
+ */
+public class LingerMonitor {
+
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;
+    private static final String TAG = LingerMonitor.class.getSimpleName();
+
+    private static final HashMap<String, Integer> sTransportNames = makeTransportToNameMap();
+    private static final Intent CELLULAR_SETTINGS = new Intent().setComponent(new ComponentName(
+            "com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity"));
+
+    private static final int NOTIFY_TYPE_NONE = 0;
+    private static final int NOTIFY_TYPE_NOTIFICATION = 1;
+    private static final int NOTIFY_TYPE_TOAST = 2;
+
+    private static SparseArray<String> sNotifyTypeNames = MessageUtils.findMessageNames(
+            new Class[] { LingerMonitor.class }, new String[]{ "NOTIFY_TYPE_" });
+
+    private final Context mContext;
+    private final NetworkNotificationManager mNotifier;
+
+    /** Current notifications. Maps the netId we switched away from to the netId we switched to. */
+    private final SparseIntArray mNotifications = new SparseIntArray();
+
+    /** Whether we ever notified that we switched away from a particular network. */
+    private final SparseBooleanArray mEverNotified = new SparseBooleanArray();
+
+    public LingerMonitor(Context context, NetworkNotificationManager notifier) {
+        mContext = context;
+        mNotifier = notifier;
+    }
+
+    private static HashMap<String, Integer> makeTransportToNameMap() {
+        SparseArray<String> numberToName = MessageUtils.findMessageNames(
+            new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" });
+        HashMap<String, Integer> nameToNumber = new HashMap<>();
+        for (int i = 0; i < numberToName.size(); i++) {
+            // MessageUtils will fail to initialize if there are duplicate constant values, so there
+            // are no duplicates here.
+            nameToNumber.put(numberToName.valueAt(i), numberToName.keyAt(i));
+        }
+        return nameToNumber;
+    }
+
+    private static boolean hasTransport(NetworkAgentInfo nai, int transport) {
+        return nai.networkCapabilities.hasTransport(transport);
+    }
+
+    private int getNotificationSource(NetworkAgentInfo toNai) {
+        for (int i = 0; i < mNotifications.size(); i++) {
+            if (mNotifications.valueAt(i) == toNai.network.netId) {
+                return mNotifications.keyAt(i);
+            }
+        }
+        return NETID_UNSET;
+    }
+
+    private boolean everNotified(NetworkAgentInfo nai) {
+        return mEverNotified.get(nai.network.netId, false);
+    }
+
+    private boolean isNotificationEnabled(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+        // TODO: Evaluate moving to CarrierConfigManager.
+        String[] notifySwitches = mContext.getResources().getStringArray(
+                com.android.internal.R.array.config_networkNotifySwitches);
+
+        if (VDBG) {
+            Log.d(TAG, "Notify on network switches: " + Arrays.toString(notifySwitches));
+        }
+
+        for (String notifySwitch : notifySwitches) {
+            if (TextUtils.isEmpty(notifySwitch)) continue;
+            String[] transports = notifySwitch.split("-", 2);
+            if (transports.length != 2) {
+                Log.e(TAG, "Invalid network switch notification configuration: " + notifySwitch);
+                continue;
+            }
+            int fromTransport = sTransportNames.get("TRANSPORT_" + transports[0]);
+            int toTransport = sTransportNames.get("TRANSPORT_" + transports[1]);
+            if (hasTransport(fromNai, fromTransport) && hasTransport(toNai, toTransport)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private void showNotification(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+        PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
+                mContext, 0, CELLULAR_SETTINGS, PendingIntent.FLAG_CANCEL_CURRENT, null,
+                UserHandle.CURRENT);
+
+        mNotifier.showNotification(fromNai.network.netId, NotificationType.NETWORK_SWITCH,
+                fromNai, toNai, pendingIntent, true);
+    }
+
+    // Removes any notification that was put up as a result of switching to nai.
+    private void maybeStopNotifying(NetworkAgentInfo nai) {
+        int fromNetId = getNotificationSource(nai);
+        if (fromNetId != NETID_UNSET) {
+            mNotifications.delete(fromNetId);
+            mNotifier.clearNotification(fromNetId);
+            // Toasts can't be deleted.
+        }
+    }
+
+    // Notify the user of a network switch using a notification or a toast.
+    private void notify(NetworkAgentInfo fromNai, NetworkAgentInfo toNai, boolean forceToast) {
+        boolean notify = false;
+        int notifyType = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_networkNotifySwitchType);
+
+        if (notifyType == NOTIFY_TYPE_NOTIFICATION && forceToast) {
+            notifyType = NOTIFY_TYPE_TOAST;
+        }
+
+        switch (notifyType) {
+            case NOTIFY_TYPE_NONE:
+                break;
+            case NOTIFY_TYPE_NOTIFICATION:
+                showNotification(fromNai, toNai);
+                notify = true;
+                break;
+            case NOTIFY_TYPE_TOAST:
+                mNotifier.showToast(fromNai, toNai);
+                notify = true;
+                break;
+            default:
+                Log.e(TAG, "Unknown notify type " + notifyType);
+        }
+
+        if (VDBG) {
+            Log.d(TAG, "Notify type: " + sNotifyTypeNames.get(notifyType, "" + notifyType));
+        }
+
+        if (notify) {
+            if (DBG) {
+                Log.d(TAG, "Notifying switch from=" + fromNai.name() + " to=" + toNai.name() +
+                        " type=" + sNotifyTypeNames.get(notifyType, "unknown(" + notifyType + ")"));
+            }
+            mNotifications.put(fromNai.network.netId, toNai.network.netId);
+            mEverNotified.put(fromNai.network.netId, true);
+        }
+    }
+
+    // The default network changed from fromNai to toNai due to a change in score.
+    public void noteLingerDefaultNetwork(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+        if (VDBG) {
+            Log.d(TAG, "noteLingerDefaultNetwork from=" + fromNai.name() +
+                    " everValidated=" + fromNai.everValidated +
+                    " lastValidated=" + fromNai.lastValidated +
+                    " to=" + toNai.name());
+        }
+
+        // If we are currently notifying the user because the device switched to fromNai, now that
+        // we are switching away from it we should remove the notification. This includes the case
+        // where we switch back to toNai because its score improved again (e.g., because it regained
+        // Internet access).
+        maybeStopNotifying(fromNai);
+
+        // If this network never validated, don't notify. Otherwise, we could do things like:
+        //
+        // 1. Unvalidated wifi connects.
+        // 2. Unvalidated mobile data connects.
+        // 3. Cell validates, and we show a notification.
+        // or:
+        // 1. User connects to wireless printer.
+        // 2. User turns on cellular data.
+        // 3. We show a notification.
+        if (!fromNai.everValidated) return;
+
+        // If this network is a captive portal, don't notify. This cannot happen on initial connect
+        // to a captive portal, because the everValidated check above will fail. However, it can
+        // happen if the captive portal reasserts itself (e.g., because its timeout fires). In that
+        // case, as soon as the captive portal reasserts itself, we'll show a sign-in notification.
+        // We don't want to overwrite that notification with this one; the user has already been
+        // notified, and of the two, the captive portal notification is the more useful one because
+        // it allows the user to sign in to the captive portal. In this case, display a toast
+        // in addition to the captive portal notification.
+        //
+        // Note that if the network we switch to is already up when the captive portal reappears,
+        // this won't work because NetworkMonitor tells ConnectivityService that the network is
+        // unvalidated (causing a switch) before asking it to show the sign in notification. In this
+        // case, the toast won't show and we'll only display the sign in notification. This is the
+        // best we can do at this time.
+        boolean forceToast = fromNai.networkCapabilities.hasCapability(
+                NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL);
+
+        // Only show the notification once, in order to avoid irritating the user every time.
+        // TODO: should we do this?
+        if (everNotified(fromNai)) {
+            if (VDBG) {
+                Log.d(TAG, "Not notifying handover from " + fromNai.name() + ", already notified");
+            }
+            return;
+        }
+
+        if (isNotificationEnabled(fromNai, toNai)) {
+            notify(fromNai, toNai, forceToast);
+        }
+    }
+
+    public void noteDisconnect(NetworkAgentInfo nai) {
+        mNotifications.delete(nai.network.netId);
+        mEverNotified.delete(nai.network.netId);
+        maybeStopNotifying(nai);
+        // No need to cancel notifications on nai: NetworkMonitor does that on disconnect.
+    }
+}
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index 7a25df6..b0330b9 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -411,14 +411,17 @@
 
     /**
      * Cancel lingering. Called by ConnectivityService when a request is added to this network.
+     * Returns true if the given request was lingering on this network, false otherwise.
      */
-    public void unlingerRequest(NetworkRequest request) {
+    public boolean unlingerRequest(NetworkRequest request) {
         LingerTimer timer = mLingerTimerForRequest.get(request.requestId);
         if (timer != null) {
             if (VDBG) Log.d(TAG, "Removing LingerTimer " + timer + " from " + this.name());
             mLingerTimers.remove(timer);
             mLingerTimerForRequest.remove(request.requestId);
+            return true;
         }
+        return false;
     }
 
     public long getLingerExpiry() {
diff --git a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
index 4680a8a..99926a9 100644
--- a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
@@ -19,6 +19,7 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.widget.Toast;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
@@ -34,9 +35,9 @@
 
 public class NetworkNotificationManager {
 
-    public static enum NotificationType { SIGN_IN, NO_INTERNET; };
+    public static enum NotificationType { SIGN_IN, NO_INTERNET, NETWORK_SWITCH };
 
-    private static final String NOTIFICATION_ID = "CaptivePortal.Notification";
+    private static final String NOTIFICATION_ID = "Connectivity.Notification";
 
     private static final String TAG = NetworkNotificationManager.class.getSimpleName();
     private static final boolean DBG = true;
@@ -90,9 +91,15 @@
      * @param id an identifier that uniquely identifies this notification.  This must match
      *         between show and hide calls.  We use the NetID value but for legacy callers
      *         we concatenate the range of types with the range of NetIDs.
+     * @param nai the network with which the notification is associated. For a SIGN_IN or
+     *         NO_INTERNET notification, this is the network we're connecting to. For a
+     *         NETWORK_SWITCH notification it's the network that we switched from. When this network
+     *         disconnects the notification is removed.
+     * @param switchToNai for a NETWORK_SWITCH notification, the network we are switching to. Null
+     *         in all other cases. Only used to determine the text of the notification.
      */
-    public void showNotification(int id, NotificationType notifyType,
-            NetworkAgentInfo nai, PendingIntent intent, boolean highPriority) {
+    public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
+            NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) {
         int transportType;
         String extraInfo;
         if (nai != null) {
@@ -136,29 +143,42 @@
                     details = r.getString(R.string.network_available_sign_in_detailed, extraInfo);
                     break;
             }
+        } else if (notifyType == NotificationType.NETWORK_SWITCH) {
+            String fromTransport = getTransportName(transportType);
+            String toTransport = getTransportName(getFirstTransportType(switchToNai));
+            title = r.getString(R.string.network_switch_metered, toTransport);
+            details = r.getString(R.string.network_switch_metered_detail, toTransport,
+                    fromTransport);
         } else {
             Slog.wtf(TAG, "Unknown notification type " + notifyType + "on network transport "
                     + getTransportName(transportType));
             return;
         }
 
-        Notification notification = new Notification.Builder(mContext)
-                .setWhen(0)
+        Notification.Builder builder = new Notification.Builder(mContext)
+                .setWhen(System.currentTimeMillis())
+                .setShowWhen(notifyType == NotificationType.NETWORK_SWITCH)
                 .setSmallIcon(icon)
                 .setAutoCancel(true)
                 .setTicker(title)
                 .setColor(mContext.getColor(
                         com.android.internal.R.color.system_notification_accent_color))
                 .setContentTitle(title)
-                .setContentText(details)
                 .setContentIntent(intent)
                 .setLocalOnly(true)
                 .setPriority(highPriority ?
                         Notification.PRIORITY_HIGH :
                         Notification.PRIORITY_DEFAULT)
                 .setDefaults(highPriority ? Notification.DEFAULT_ALL : 0)
-                .setOnlyAlertOnce(true)
-                .build();
+                .setOnlyAlertOnce(true);
+
+        if (notifyType == NotificationType.NETWORK_SWITCH) {
+            builder.setStyle(new Notification.BigTextStyle().bigText(details));
+        } else {
+            builder.setContentText(details);
+        }
+
+        Notification notification = builder.build();
 
         try {
             mNotificationManager.notifyAsUser(NOTIFICATION_ID, id, notification, UserHandle.ALL);
@@ -185,9 +205,17 @@
         if (visible) {
             Intent intent = new Intent(action);
             PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
-            showNotification(id, NotificationType.SIGN_IN, null, pendingIntent, false);
+            showNotification(id, NotificationType.SIGN_IN, null, null, pendingIntent, false);
         } else {
             clearNotification(id);
         }
     }
+
+    public void showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+        String fromTransport = getTransportName(getFirstTransportType(fromNai));
+        String toTransport = getTransportName(getFirstTransportType(toNai));
+        String text = mContext.getResources().getString(
+                R.string.network_switch_metered_toast, fromTransport, toTransport);
+        Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
+    }
 }